Adding API mocking code. API error handling refactoring
This commit is contained in:
parent
d83fc71810
commit
0d261b3452
@ -36,10 +36,23 @@
|
||||
7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60726998DCF009B0BC4 /* Alert.swift */; };
|
||||
7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */; };
|
||||
7A503C03269F382F002C1A0D /* login_success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A503C02269F382F002C1A0D /* login_success.json */; };
|
||||
7A503C05269F494C002C1A0D /* login_invalid_params.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A503C04269F494C002C1A0D /* login_invalid_params.json */; };
|
||||
7A503C07269F49F4002C1A0D /* login_wrong_credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A503C06269F49F4002C1A0D /* login_wrong_credentials.json */; };
|
||||
7A683999269612EA00B2188A /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A683998269612EA00B2188A /* Response.swift */; };
|
||||
7A68399A269612EA00B2188A /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A683998269612EA00B2188A /* Response.swift */; };
|
||||
7A971F0626AD6F2F007E527B /* ApiMethodMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F0526AD6F2F007E527B /* ApiMethodMock.swift */; };
|
||||
7A971F0826AD7084007E527B /* LoginMethodMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F0726AD7084007E527B /* LoginMethodMock.swift */; };
|
||||
7A971F0A26AD74FD007E527B /* ApiMethodMockProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F0926AD74FD007E527B /* ApiMethodMockProtocol.swift */; };
|
||||
7A971F0D26AD7D4C007E527B /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F0C26AD7D4C007E527B /* AnyEncodable.swift */; };
|
||||
7A971F0E26AD7D4C007E527B /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F0C26AD7D4C007E527B /* AnyEncodable.swift */; };
|
||||
7A971F1526AD8AEB007E527B /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1026AD8AEB007E527B /* Initialization.swift */; };
|
||||
7A971F1626AD8AEB007E527B /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1026AD8AEB007E527B /* Initialization.swift */; };
|
||||
7A971F1726AD8AEB007E527B /* Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1126AD8AEB007E527B /* Querying.swift */; };
|
||||
7A971F1826AD8AEB007E527B /* Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1126AD8AEB007E527B /* Querying.swift */; };
|
||||
7A971F1926AD8AEB007E527B /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1226AD8AEB007E527B /* JSON.swift */; };
|
||||
7A971F1A26AD8AEB007E527B /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1226AD8AEB007E527B /* JSON.swift */; };
|
||||
7A971F1D26AD8AEB007E527B /* Merging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1426AD8AEB007E527B /* Merging.swift */; };
|
||||
7A971F1E26AD8AEB007E527B /* Merging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1426AD8AEB007E527B /* Merging.swift */; };
|
||||
7A971F2026ADC351007E527B /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1F26ADC351007E527B /* ApiError.swift */; };
|
||||
7A971F2126ADC351007E527B /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A971F1F26ADC351007E527B /* ApiError.swift */; };
|
||||
7ACD05D72695C08A00557667 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD05D62695C08A00557667 /* Constants.swift */; };
|
||||
7ACD05D82695C08A00557667 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD05D62695C08A00557667 /* Constants.swift */; };
|
||||
7AEFAEED26985A3400ED2C85 /* ACProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFAEEC26985A3400ED2C85 /* ACProgressView.swift */; };
|
||||
@ -117,9 +130,16 @@
|
||||
7A40D60726998DCF009B0BC4 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
||||
7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
|
||||
7A503C02269F382F002C1A0D /* login_success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = login_success.json; sourceTree = "<group>"; };
|
||||
7A503C04269F494C002C1A0D /* login_invalid_params.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = login_invalid_params.json; sourceTree = "<group>"; };
|
||||
7A503C06269F49F4002C1A0D /* login_wrong_credentials.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = login_wrong_credentials.json; sourceTree = "<group>"; };
|
||||
7A683998269612EA00B2188A /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
|
||||
7A971F0526AD6F2F007E527B /* ApiMethodMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiMethodMock.swift; sourceTree = "<group>"; };
|
||||
7A971F0726AD7084007E527B /* LoginMethodMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMethodMock.swift; sourceTree = "<group>"; };
|
||||
7A971F0926AD74FD007E527B /* ApiMethodMockProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiMethodMockProtocol.swift; sourceTree = "<group>"; };
|
||||
7A971F0C26AD7D4C007E527B /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
|
||||
7A971F1026AD8AEB007E527B /* Initialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = "<group>"; };
|
||||
7A971F1126AD8AEB007E527B /* Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Querying.swift; sourceTree = "<group>"; };
|
||||
7A971F1226AD8AEB007E527B /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
7A971F1426AD8AEB007E527B /* Merging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Merging.swift; sourceTree = "<group>"; };
|
||||
7A971F1F26ADC351007E527B /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = "<group>"; };
|
||||
7ACD05D62695C08A00557667 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
7AEFAEEC26985A3400ED2C85 /* ACProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACProgressView.swift; sourceTree = "<group>"; };
|
||||
7AF552D82696E5C100578083 /* ApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiTests.swift; sourceTree = "<group>"; };
|
||||
@ -179,6 +199,7 @@
|
||||
7A40D5782691C6D7009B0BC4 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A971F0B26AD7D27007E527B /* ThirdParty */,
|
||||
7A40D6002694FF4C009B0BC4 /* Utils */,
|
||||
7A40D5FC2693A90F009B0BC4 /* Extensions */,
|
||||
7A40D5EB2693A1C3009B0BC4 /* ViewModels */,
|
||||
@ -266,8 +287,7 @@
|
||||
7A40D5F42693A63A009B0BC4 /* AutoCat2Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A503C00269F370A002C1A0D /* Responses */,
|
||||
7A40D60A2699A04F009B0BC4 /* Mocks */,
|
||||
7A971F0326AD6EA1007E527B /* Api */,
|
||||
7A40D5F52693A63A009B0BC4 /* SettingsTests.swift */,
|
||||
7AF552D82696E5C100578083 /* ApiTests.swift */,
|
||||
);
|
||||
@ -287,6 +307,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A40D6012694FF5D009B0BC4 /* Api.swift */,
|
||||
7A971F1F26ADC351007E527B /* ApiError.swift */,
|
||||
7ACD05D62695C08A00557667 /* Constants.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
@ -295,7 +316,8 @@
|
||||
7A40D60A2699A04F009B0BC4 /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */,
|
||||
7A971F0526AD6F2F007E527B /* ApiMethodMock.swift */,
|
||||
7A971F0726AD7084007E527B /* LoginMethodMock.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
@ -311,13 +333,50 @@
|
||||
7A503C01269F3797002C1A0D /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A503C06269F49F4002C1A0D /* login_wrong_credentials.json */,
|
||||
7A503C04269F494C002C1A0D /* login_invalid_params.json */,
|
||||
7A503C02269F382F002C1A0D /* login_success.json */,
|
||||
);
|
||||
path = Login;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A971F0326AD6EA1007E527B /* Api */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A971F0426AD6ED6007E527B /* Lib */,
|
||||
7A40D60A2699A04F009B0BC4 /* Mocks */,
|
||||
7A503C00269F370A002C1A0D /* Responses */,
|
||||
);
|
||||
path = Api;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A971F0426AD6ED6007E527B /* Lib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */,
|
||||
7A971F0926AD74FD007E527B /* ApiMethodMockProtocol.swift */,
|
||||
);
|
||||
path = Lib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A971F0B26AD7D27007E527B /* ThirdParty */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A971F0F26AD8AEB007E527B /* GenericJSON */,
|
||||
7A971F0C26AD7D4C007E527B /* AnyEncodable.swift */,
|
||||
);
|
||||
path = ThirdParty;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A971F0F26AD8AEB007E527B /* GenericJSON */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A971F1026AD8AEB007E527B /* Initialization.swift */,
|
||||
7A971F1126AD8AEB007E527B /* Querying.swift */,
|
||||
7A971F1226AD8AEB007E527B /* JSON.swift */,
|
||||
7A971F1426AD8AEB007E527B /* Merging.swift */,
|
||||
);
|
||||
path = GenericJSON;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -498,9 +557,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7A503C07269F49F4002C1A0D /* login_wrong_credentials.json in Resources */,
|
||||
7A503C03269F382F002C1A0D /* login_success.json in Resources */,
|
||||
7A503C05269F494C002C1A0D /* login_invalid_params.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -512,13 +569,19 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7A40D5E926938BEC009B0BC4 /* AuthView.swift in Sources */,
|
||||
7A971F0D26AD7D4C007E527B /* AnyEncodable.swift in Sources */,
|
||||
7ACD05D72695C08A00557667 /* Constants.swift in Sources */,
|
||||
7A971F1926AD8AEB007E527B /* JSON.swift in Sources */,
|
||||
7A971F1D26AD8AEB007E527B /* Merging.swift in Sources */,
|
||||
7A40D5E326924B09009B0BC4 /* Settings.swift in Sources */,
|
||||
7AEFAEED26985A3400ED2C85 /* ACProgressView.swift in Sources */,
|
||||
7A40D5A02691C6D8009B0BC4 /* AutoCat2App.swift in Sources */,
|
||||
7A683999269612EA00B2188A /* Response.swift in Sources */,
|
||||
7A971F1526AD8AEB007E527B /* Initialization.swift in Sources */,
|
||||
7A971F1726AD8AEB007E527B /* Querying.swift in Sources */,
|
||||
7A40D60826998DCF009B0BC4 /* Alert.swift in Sources */,
|
||||
7A40D5A42691C6D8009B0BC4 /* Persistence.swift in Sources */,
|
||||
7A971F2026ADC351007E527B /* ApiError.swift in Sources */,
|
||||
7A40D5ED2693A1EA009B0BC4 /* AuthVM.swift in Sources */,
|
||||
7A40D59E2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */,
|
||||
7A40D5E126924AEC009B0BC4 /* User.swift in Sources */,
|
||||
@ -533,13 +596,19 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7A40D5EA26938BEC009B0BC4 /* AuthView.swift in Sources */,
|
||||
7A971F0E26AD7D4C007E527B /* AnyEncodable.swift in Sources */,
|
||||
7ACD05D82695C08A00557667 /* Constants.swift in Sources */,
|
||||
7A971F1A26AD8AEB007E527B /* JSON.swift in Sources */,
|
||||
7A971F1E26AD8AEB007E527B /* Merging.swift in Sources */,
|
||||
7A40D5A12691C6D8009B0BC4 /* AutoCat2App.swift in Sources */,
|
||||
7AEFAEEE26985A3400ED2C85 /* ACProgressView.swift in Sources */,
|
||||
7A40D5A52691C6D8009B0BC4 /* Persistence.swift in Sources */,
|
||||
7A68399A269612EA00B2188A /* Response.swift in Sources */,
|
||||
7A971F1626AD8AEB007E527B /* Initialization.swift in Sources */,
|
||||
7A971F1826AD8AEB007E527B /* Querying.swift in Sources */,
|
||||
7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */,
|
||||
7A40D5E526924B0C009B0BC4 /* User.swift in Sources */,
|
||||
7A971F2126ADC351007E527B /* ApiError.swift in Sources */,
|
||||
7A40D5EE2693A1EA009B0BC4 /* AuthVM.swift in Sources */,
|
||||
7A40D59F2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */,
|
||||
7A40D5A32691C6D8009B0BC4 /* ContentView.swift in Sources */,
|
||||
@ -570,8 +639,11 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */,
|
||||
7A971F0826AD7084007E527B /* LoginMethodMock.swift in Sources */,
|
||||
7AF552D92696E5C100578083 /* ApiTests.swift in Sources */,
|
||||
7A971F0A26AD74FD007E527B /* ApiMethodMockProtocol.swift in Sources */,
|
||||
7A40D5F62693A63A009B0BC4 /* SettingsTests.swift in Sources */,
|
||||
7A971F0626AD6F2F007E527B /* ApiMethodMock.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
<key>AutoCat2 (iOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>AutoCat2 (macOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>AutoCatCore.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
||||
7
AutoCat2Tests/Api/Lib/ApiMethodMockProtocol.swift
Normal file
7
AutoCat2Tests/Api/Lib/ApiMethodMockProtocol.swift
Normal file
@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol ApiMethodMockProtocol {
|
||||
var path: String { get }
|
||||
var httpMethod: String { get }
|
||||
func response(headers: [String: String], params: [String: Any]) -> (status: Int, data: Data?)
|
||||
}
|
||||
96
AutoCat2Tests/Api/Lib/MockURLProtocol.swift
Normal file
96
AutoCat2Tests/Api/Lib/MockURLProtocol.swift
Normal file
@ -0,0 +1,96 @@
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
public var queryParameters: [String: String]? {
|
||||
guard
|
||||
let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
|
||||
let queryItems = components.queryItems else { return nil }
|
||||
return queryItems.reduce(into: [String: String]()) { (result, item) in
|
||||
result[item.name] = item.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLRequest {
|
||||
func bodySteamAsJSON() -> [String: Any]? {
|
||||
guard let bodyStream = self.httpBodyStream else { return nil }
|
||||
|
||||
bodyStream.open()
|
||||
|
||||
// Will read 16 chars per iteration. Can use bigger buffer if needed
|
||||
let bufferSize: Int = 16
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
var dat = Data()
|
||||
|
||||
while bodyStream.hasBytesAvailable {
|
||||
let readDat = bodyStream.read(buffer, maxLength: bufferSize)
|
||||
dat.append(buffer, count: readDat)
|
||||
}
|
||||
|
||||
buffer.deallocate()
|
||||
bodyStream.close()
|
||||
|
||||
return try? JSONSerialization.jsonObject(with: dat, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: Any]
|
||||
}
|
||||
}
|
||||
|
||||
class MockURLProtocol: URLProtocol {
|
||||
|
||||
static var baseUrl: String = ""
|
||||
static var apiMethodMocks: [ApiMethodMockProtocol] = []
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
guard let requestUrl = request.url else { return }
|
||||
|
||||
let methodMock = MockURLProtocol.apiMethodMocks.first {
|
||||
return request.url?.absoluteString == MockURLProtocol.baseUrl + $0.path
|
||||
&& request.httpMethod == $0.httpMethod
|
||||
}
|
||||
|
||||
guard let methodMock = methodMock else {
|
||||
if let response = HTTPURLResponse(url: requestUrl, statusCode: 404, httpVersion: "HTTP/2", headerFields: [:]) {
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Assuming we use url parameters in GET requests and JSON-encoded body in everything else
|
||||
var params: [String: Any] = [:]
|
||||
if request.httpBodyStream != nil {
|
||||
if let bodyDict = request.bodySteamAsJSON() {
|
||||
params = bodyDict
|
||||
}
|
||||
} else {
|
||||
if let urlParams = requestUrl.queryParameters {
|
||||
params = urlParams
|
||||
}
|
||||
}
|
||||
|
||||
let result = methodMock.response(headers: request.allHTTPHeaderFields ?? [:], params: params)
|
||||
guard let response = HTTPURLResponse(url: requestUrl, statusCode: result.status, httpVersion: "HTTP/2", headerFields: [:]) else { return }
|
||||
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
|
||||
if let data = result.data {
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
}
|
||||
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
|
||||
//client?.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
|
||||
override func stopLoading() {
|
||||
|
||||
}
|
||||
}
|
||||
39
AutoCat2Tests/Api/Mocks/ApiMethodMock.swift
Normal file
39
AutoCat2Tests/Api/Mocks/ApiMethodMock.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import AutoCat2
|
||||
|
||||
open class ApiMethodMock: ApiMethodMockProtocol {
|
||||
|
||||
private(set) var path: String
|
||||
private(set) var httpMethod: String
|
||||
|
||||
init(httpMethod: String, path: String) {
|
||||
self.httpMethod = httpMethod
|
||||
self.path = path
|
||||
}
|
||||
|
||||
func readData(from path: String) -> Data? {
|
||||
guard let url = Bundle(for: type(of: self)).url(forResource: path, withExtension: "json") else { return nil }
|
||||
return try? Data(contentsOf: url)
|
||||
}
|
||||
|
||||
func error(message: String, code: ApiErrorCode? = nil) -> Data? {
|
||||
var errorData: [String: AnyEncodable] = [
|
||||
"success": false,
|
||||
"error": AnyEncodable(message)
|
||||
]
|
||||
|
||||
if let code = code {
|
||||
errorData["errorCode"] = AnyEncodable(code)
|
||||
}
|
||||
|
||||
return try? JSONEncoder().encode(errorData)
|
||||
}
|
||||
|
||||
func notFoundResponse() -> (status: Int, data: Data?) {
|
||||
return (status: 404, data: self.error(message: "Not found"))
|
||||
}
|
||||
|
||||
open func response(headers: [String : String], params: [String : Any]) -> (status: Int, data: Data?) {
|
||||
return self.notFoundResponse()
|
||||
}
|
||||
}
|
||||
24
AutoCat2Tests/Api/Mocks/LoginMethodMock.swift
Normal file
24
AutoCat2Tests/Api/Mocks/LoginMethodMock.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
class LoginMethodMock: ApiMethodMock {
|
||||
private var login: String
|
||||
private var password: String
|
||||
|
||||
init(httpMethod: String, path: String, login: String, password: String) {
|
||||
self.login = login
|
||||
self.password = password
|
||||
super.init(httpMethod: httpMethod, path: path)
|
||||
}
|
||||
|
||||
override func response(headers: [String : String], params: [String : Any]) -> (status: Int, data: Data?) {
|
||||
guard let login = params["email"] as? String, let password = params["password"] as? String else {
|
||||
return (status: 400, data: self.error(message: "Invalid parameters"))
|
||||
}
|
||||
|
||||
if login != self.login || password != self.password {
|
||||
return (status: 200, data: self.error(message: "Incorrect login or password", code: .invalidLoginOrPassword))
|
||||
}
|
||||
|
||||
return (status: 200, data: readData(from: "login_success"))
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,16 @@ import AutoCat2
|
||||
class ApiTests: XCTestCase {
|
||||
|
||||
private var api: Api!
|
||||
|
||||
private let testLogin = "test@gmail.com"
|
||||
private let testPassword = "12345"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
MockURLProtocol.baseUrl = Constants.baseUrl
|
||||
MockURLProtocol.apiMethodMocks = [
|
||||
LoginMethodMock(httpMethod: "POST", path: "user/login", login: self.testLogin, password: self.testPassword)
|
||||
]
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
sessionConfig.protocolClasses = [MockURLProtocol.self]
|
||||
let session = URLSession(configuration: sessionConfig)
|
||||
@ -13,31 +21,25 @@ class ApiTests: XCTestCase {
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
MockURLProtocol.baseUrl = ""
|
||||
MockURLProtocol.apiMethodMocks = []
|
||||
}
|
||||
|
||||
func testLoginSuccess() async throws {
|
||||
MockURLProtocol.responseType = MockResponseType.loginSuccess
|
||||
let user = try await self.api.login(email: "", password: "")
|
||||
let user = try await self.api.login(email: self.testLogin, password: self.testPassword)
|
||||
XCTAssertTrue(!user.token.isEmpty)
|
||||
}
|
||||
|
||||
func testLoginInvalidParams() async throws {
|
||||
MockURLProtocol.responseType = MockResponseType.loginWrongCredentials
|
||||
do {
|
||||
_ = try await self.api.login(email: "", password: "")
|
||||
} catch {
|
||||
} catch let error as ApiError {
|
||||
XCTAssertTrue(error.code == .invalidLoginOrPassword)
|
||||
return
|
||||
} catch {
|
||||
XCTFail("Wrong exception type")
|
||||
}
|
||||
|
||||
XCTFail("Exception expected")
|
||||
}
|
||||
|
||||
// func testPerformanceExample() throws {
|
||||
// // This is an example of a performance test case.
|
||||
// self.measure {
|
||||
// // Put the code you want to measure the time of here.
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum MockResponseType: String {
|
||||
case loginSuccess = "login_success"
|
||||
case loginWrongCredentials = "login_wrong_credentials"
|
||||
case
|
||||
}
|
||||
|
||||
class MockURLProtocol: URLProtocol {
|
||||
|
||||
static var responseType: MockResponseType?
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
// To check if this protocol can handle the given request.
|
||||
return true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
// Here you return the canonical version of the request but most of the time you pass the orignal one.
|
||||
return request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
|
||||
guard let respType = MockURLProtocol.responseType else { return }
|
||||
guard let jsonUrl = Bundle(for: type(of: self)).url(forResource: respType.rawValue, withExtension: "json") else { return }
|
||||
guard let response = HTTPURLResponse(url: jsonUrl, statusCode: 200, httpVersion: "HTTP/2", headerFields: [:]) else { return }
|
||||
|
||||
let data = try? Data(contentsOf: jsonUrl)
|
||||
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
|
||||
if let data = data {
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
}
|
||||
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
|
||||
//client?.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
|
||||
override func stopLoading() {
|
||||
// This is called if the request gets canceled or completed.
|
||||
print("")
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid parameters"
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Incorrect email or password"
|
||||
}
|
||||
@ -2,16 +2,22 @@ import CoreLocation
|
||||
|
||||
extension NSError {
|
||||
public var displayMessage: (title: String, body: String) {
|
||||
if let description = self.userInfo[NSLocalizedDescriptionKey] as? String {
|
||||
return (title: "Error", body: description)
|
||||
} else if let failure = self.userInfo[NSLocalizedFailureErrorKey] as? String, let reason = self.localizedFailureReason {
|
||||
if let recovery = self.localizedRecoverySuggestion {
|
||||
return (title: failure, body: reason + "\n" + recovery)
|
||||
} else {
|
||||
return (title: failure, body: reason)
|
||||
}
|
||||
// if let description = self.userInfo[NSLocalizedDescriptionKey] as? String {
|
||||
// return (title: "Error", body: description)
|
||||
// } else if let failure = self.userInfo[NSLocalizedFailureErrorKey] as? String, let reason = self.localizedFailureReason {
|
||||
// if let recovery = self.localizedRecoverySuggestion {
|
||||
// return (title: failure, body: reason + "\n" + recovery)
|
||||
// } else {
|
||||
// return (title: failure, body: reason)
|
||||
// }
|
||||
// } else {
|
||||
// return (title: "Error", body: "")
|
||||
// }
|
||||
|
||||
if let recovery = self.localizedRecoverySuggestion {
|
||||
return (title: "Error", body: self.localizedDescription + "\n" + recovery)
|
||||
} else {
|
||||
return (title: "Error", body: "")
|
||||
return (title: "Error", body: self.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,13 @@ class Response<T>: Decodable where T: Decodable {
|
||||
let success: Bool
|
||||
let data: T?
|
||||
let error: String?
|
||||
let errorCode: ApiErrorCode?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case success
|
||||
case data
|
||||
case error
|
||||
case errorCode
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
@ -17,8 +19,10 @@ class Response<T>: Decodable where T: Decodable {
|
||||
if success {
|
||||
data = try container.decode(T.self, forKey: .data)
|
||||
error = nil
|
||||
errorCode = nil
|
||||
} else {
|
||||
error = try container.decode(String.self, forKey: .error)
|
||||
errorCode = try container.decodeIfPresent(ApiErrorCode.self, forKey: .errorCode)
|
||||
data = nil
|
||||
}
|
||||
}
|
||||
|
||||
273
Shared/ThirdParty/AnyEncodable.swift
vendored
Normal file
273
Shared/ThirdParty/AnyEncodable.swift
vendored
Normal file
@ -0,0 +1,273 @@
|
||||
#if canImport(Foundation)
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
/**
|
||||
A type-erased `Encodable` value.
|
||||
|
||||
The `AnyEncodable` type forwards encoding responsibilities
|
||||
to an underlying value, hiding its specific underlying type.
|
||||
|
||||
You can encode mixed-type values in dictionaries
|
||||
and other collections that require `Encodable` conformance
|
||||
by declaring their contained type to be `AnyEncodable`:
|
||||
|
||||
let dictionary: [String: AnyEncodable] = [
|
||||
"boolean": true,
|
||||
"integer": 1,
|
||||
"double": 3.141592653589793,
|
||||
"string": "string",
|
||||
"array": [1, 2, 3],
|
||||
"nested": [
|
||||
"a": "alpha",
|
||||
"b": "bravo",
|
||||
"c": "charlie"
|
||||
]
|
||||
]
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let json = try! encoder.encode(dictionary)
|
||||
*/
|
||||
#if swift(>=5.1)
|
||||
@frozen public struct AnyEncodable: Encodable {
|
||||
public let value: Any
|
||||
|
||||
public init<T>(_ value: T?) {
|
||||
self.value = value ?? ()
|
||||
}
|
||||
}
|
||||
#else
|
||||
public struct AnyEncodable: Encodable {
|
||||
public let value: Any
|
||||
|
||||
public init<T>(_ value: T?) {
|
||||
self.value = value ?? ()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if swift(>=4.2)
|
||||
@usableFromInline
|
||||
protocol _AnyEncodable {
|
||||
var value: Any { get }
|
||||
init<T>(_ value: T?)
|
||||
}
|
||||
#else
|
||||
protocol _AnyEncodable {
|
||||
var value: Any { get }
|
||||
init<T>(_ value: T?)
|
||||
}
|
||||
#endif
|
||||
|
||||
extension AnyEncodable: _AnyEncodable {}
|
||||
|
||||
// MARK: - Encodable
|
||||
|
||||
extension _AnyEncodable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch value {
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
case let number as NSNumber:
|
||||
try encode(nsnumber: number, into: &container)
|
||||
#endif
|
||||
#if canImport(Foundation)
|
||||
case is NSNull:
|
||||
try container.encodeNil()
|
||||
#endif
|
||||
case is Void:
|
||||
try container.encodeNil()
|
||||
case let bool as Bool:
|
||||
try container.encode(bool)
|
||||
case let int as Int:
|
||||
try container.encode(int)
|
||||
case let int8 as Int8:
|
||||
try container.encode(int8)
|
||||
case let int16 as Int16:
|
||||
try container.encode(int16)
|
||||
case let int32 as Int32:
|
||||
try container.encode(int32)
|
||||
case let int64 as Int64:
|
||||
try container.encode(int64)
|
||||
case let uint as UInt:
|
||||
try container.encode(uint)
|
||||
case let uint8 as UInt8:
|
||||
try container.encode(uint8)
|
||||
case let uint16 as UInt16:
|
||||
try container.encode(uint16)
|
||||
case let uint32 as UInt32:
|
||||
try container.encode(uint32)
|
||||
case let uint64 as UInt64:
|
||||
try container.encode(uint64)
|
||||
case let float as Float:
|
||||
try container.encode(float)
|
||||
case let double as Double:
|
||||
try container.encode(double)
|
||||
case let string as String:
|
||||
try container.encode(string)
|
||||
#if canImport(Foundation)
|
||||
case let date as Date:
|
||||
try container.encode(date)
|
||||
case let url as URL:
|
||||
try container.encode(url)
|
||||
#endif
|
||||
case let array as [Any?]:
|
||||
try container.encode(array.map { AnyEncodable($0) })
|
||||
case let dictionary as [String: Any?]:
|
||||
try container.encode(dictionary.mapValues { AnyEncodable($0) })
|
||||
case let enc as Encodable:
|
||||
//try container.encode(enc)
|
||||
try enc.encode(to: encoder)
|
||||
default:
|
||||
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyEncodable value cannot be encoded")
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
|
||||
switch CFNumberGetType(nsnumber) {
|
||||
case .charType:
|
||||
try container.encode(nsnumber.boolValue)
|
||||
case .sInt8Type:
|
||||
try container.encode(nsnumber.int8Value)
|
||||
case .sInt16Type:
|
||||
try container.encode(nsnumber.int16Value)
|
||||
case .sInt32Type:
|
||||
try container.encode(nsnumber.int32Value)
|
||||
case .sInt64Type:
|
||||
try container.encode(nsnumber.int64Value)
|
||||
case .shortType:
|
||||
try container.encode(nsnumber.uint16Value)
|
||||
case .longType:
|
||||
try container.encode(nsnumber.uint32Value)
|
||||
case .longLongType:
|
||||
try container.encode(nsnumber.uint64Value)
|
||||
case .intType, .nsIntegerType, .cfIndexType:
|
||||
try container.encode(nsnumber.intValue)
|
||||
case .floatType, .float32Type:
|
||||
try container.encode(nsnumber.floatValue)
|
||||
case .doubleType, .float64Type, .cgFloatType:
|
||||
try container.encode(nsnumber.doubleValue)
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled")
|
||||
throw EncodingError.invalidValue(nsnumber, context)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension AnyEncodable: Equatable {
|
||||
public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool {
|
||||
switch (lhs.value, rhs.value) {
|
||||
case is (Void, Void):
|
||||
return true
|
||||
case let (lhs as Bool, rhs as Bool):
|
||||
return lhs == rhs
|
||||
case let (lhs as Int, rhs as Int):
|
||||
return lhs == rhs
|
||||
case let (lhs as Int8, rhs as Int8):
|
||||
return lhs == rhs
|
||||
case let (lhs as Int16, rhs as Int16):
|
||||
return lhs == rhs
|
||||
case let (lhs as Int32, rhs as Int32):
|
||||
return lhs == rhs
|
||||
case let (lhs as Int64, rhs as Int64):
|
||||
return lhs == rhs
|
||||
case let (lhs as UInt, rhs as UInt):
|
||||
return lhs == rhs
|
||||
case let (lhs as UInt8, rhs as UInt8):
|
||||
return lhs == rhs
|
||||
case let (lhs as UInt16, rhs as UInt16):
|
||||
return lhs == rhs
|
||||
case let (lhs as UInt32, rhs as UInt32):
|
||||
return lhs == rhs
|
||||
case let (lhs as UInt64, rhs as UInt64):
|
||||
return lhs == rhs
|
||||
case let (lhs as Float, rhs as Float):
|
||||
return lhs == rhs
|
||||
case let (lhs as Double, rhs as Double):
|
||||
return lhs == rhs
|
||||
case let (lhs as String, rhs as String):
|
||||
return lhs == rhs
|
||||
case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]):
|
||||
return lhs == rhs
|
||||
case let (lhs as [AnyEncodable], rhs as [AnyEncodable]):
|
||||
return lhs == rhs
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyEncodable: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch value {
|
||||
case is Void:
|
||||
return String(describing: nil as Any?)
|
||||
case let value as CustomStringConvertible:
|
||||
return value.description
|
||||
default:
|
||||
return String(describing: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyEncodable: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch value {
|
||||
case let value as CustomDebugStringConvertible:
|
||||
return "AnyEncodable(\(value.debugDescription))"
|
||||
default:
|
||||
return "AnyEncodable(\(description))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyEncodable: ExpressibleByNilLiteral {}
|
||||
extension AnyEncodable: ExpressibleByBooleanLiteral {}
|
||||
extension AnyEncodable: ExpressibleByIntegerLiteral {}
|
||||
extension AnyEncodable: ExpressibleByFloatLiteral {}
|
||||
extension AnyEncodable: ExpressibleByStringLiteral {}
|
||||
#if swift(>=5.0)
|
||||
extension AnyEncodable: ExpressibleByStringInterpolation {}
|
||||
#endif
|
||||
extension AnyEncodable: ExpressibleByArrayLiteral {}
|
||||
extension AnyEncodable: ExpressibleByDictionaryLiteral {}
|
||||
|
||||
extension _AnyEncodable {
|
||||
public init(nilLiteral _: ()) {
|
||||
self.init(nil as Any?)
|
||||
}
|
||||
|
||||
public init(booleanLiteral value: Bool) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(integerLiteral value: Int) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(floatLiteral value: Double) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(extendedGraphemeClusterLiteral value: String) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(stringLiteral value: String) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(arrayLiteral elements: Any...) {
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
|
||||
self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first }))
|
||||
}
|
||||
}
|
||||
124
Shared/ThirdParty/GenericJSON/Initialization.swift
vendored
Normal file
124
Shared/ThirdParty/GenericJSON/Initialization.swift
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
|
||||
private struct InitializationError: Error {}
|
||||
|
||||
extension JSON {
|
||||
|
||||
/// Create a JSON value from anything.
|
||||
///
|
||||
/// Argument has to be a valid JSON structure: A `Double`, `Int`, `String`,
|
||||
/// `Bool`, an `Array` of those types or a `Dictionary` of those types.
|
||||
///
|
||||
/// You can also pass `nil` or `NSNull`, both will be treated as `.null`.
|
||||
public init(_ value: Any) throws {
|
||||
switch value {
|
||||
case _ as NSNull:
|
||||
self = .null
|
||||
case let opt as Optional<Any> where opt == nil:
|
||||
self = .null
|
||||
case let num as NSNumber:
|
||||
if num.isBool {
|
||||
self = .bool(num.boolValue)
|
||||
} else {
|
||||
self = .number(num.doubleValue)
|
||||
}
|
||||
case let str as String:
|
||||
self = .string(str)
|
||||
case let bool as Bool:
|
||||
self = .bool(bool)
|
||||
case let array as [Any]:
|
||||
self = .array(try array.map(JSON.init))
|
||||
case let dict as [String:Any]:
|
||||
self = .object(try dict.mapValues(JSON.init))
|
||||
default:
|
||||
throw InitializationError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON {
|
||||
|
||||
/// Create a JSON value from an `Encodable`. This will give you access to the “raw”
|
||||
/// encoded JSON value the `Encodable` is serialized into.
|
||||
public init<T: Encodable>(encodable: T) throws {
|
||||
let encoded = try JSONEncoder().encode(encodable)
|
||||
self = try JSONDecoder().decode(JSON.self, from: encoded)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByBooleanLiteral {
|
||||
|
||||
public init(booleanLiteral value: Bool) {
|
||||
self = .bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByNilLiteral {
|
||||
|
||||
public init(nilLiteral: ()) {
|
||||
self = .null
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByArrayLiteral {
|
||||
|
||||
public init(arrayLiteral elements: JSON...) {
|
||||
self = .array(elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByDictionaryLiteral {
|
||||
|
||||
public init(dictionaryLiteral elements: (String, JSON)...) {
|
||||
var object: [String:JSON] = [:]
|
||||
for (k, v) in elements {
|
||||
object[k] = v
|
||||
}
|
||||
self = .object(object)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByFloatLiteral {
|
||||
|
||||
public init(floatLiteral value: Double) {
|
||||
self = .number(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByIntegerLiteral {
|
||||
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .number(Double(value))
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByStringLiteral {
|
||||
|
||||
public init(stringLiteral value: String) {
|
||||
self = .string(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSNumber
|
||||
|
||||
extension NSNumber {
|
||||
|
||||
/// Boolean value indicating whether this `NSNumber` wraps a boolean.
|
||||
///
|
||||
/// For example, when using `NSJSONSerialization` Bool values are converted into `NSNumber` instances.
|
||||
///
|
||||
/// - seealso: https://stackoverflow.com/a/49641315/3589408
|
||||
fileprivate var isBool: Bool {
|
||||
let objCType = String(cString: self.objCType)
|
||||
if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let trueNumber = NSNumber(value: true)
|
||||
private let falseNumber = NSNumber(value: false)
|
||||
private let trueObjCType = String(cString: trueNumber.objCType)
|
||||
private let falseObjCType = String(cString: falseNumber.objCType)
|
||||
82
Shared/ThirdParty/GenericJSON/JSON.swift
vendored
Normal file
82
Shared/ThirdParty/GenericJSON/JSON.swift
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
|
||||
/// A JSON value representation. This is a bit more useful than the naïve `[String:Any]` type
|
||||
/// for JSON values, since it makes sure only valid JSON values are present & supports `Equatable`
|
||||
/// and `Codable`, so that you can compare values for equality and code and decode them into data
|
||||
/// or strings.
|
||||
@dynamicMemberLookup public enum JSON: Equatable {
|
||||
case string(String)
|
||||
case number(Double)
|
||||
case object([String:JSON])
|
||||
case array([JSON])
|
||||
case bool(Bool)
|
||||
case null
|
||||
}
|
||||
|
||||
extension JSON: Codable {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case let .array(array):
|
||||
try container.encode(array)
|
||||
case let .object(object):
|
||||
try container.encode(object)
|
||||
case let .string(string):
|
||||
try container.encode(string)
|
||||
case let .number(number):
|
||||
try container.encode(number)
|
||||
case let .bool(bool):
|
||||
try container.encode(bool)
|
||||
case .null:
|
||||
try container.encodeNil()
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if let object = try? container.decode([String: JSON].self) {
|
||||
self = .object(object)
|
||||
} else if let array = try? container.decode([JSON].self) {
|
||||
self = .array(array)
|
||||
} else if let string = try? container.decode(String.self) {
|
||||
self = .string(string)
|
||||
} else if let bool = try? container.decode(Bool.self) {
|
||||
self = .bool(bool)
|
||||
} else if let number = try? container.decode(Double.self) {
|
||||
self = .number(number)
|
||||
} else if container.decodeNil() {
|
||||
self = .null
|
||||
} else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(codingPath: decoder.codingPath, debugDescription: "Invalid JSON value.")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: CustomDebugStringConvertible {
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .string(let str):
|
||||
return str.debugDescription
|
||||
case .number(let num):
|
||||
return num.debugDescription
|
||||
case .bool(let bool):
|
||||
return bool.description
|
||||
case .null:
|
||||
return "null"
|
||||
default:
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted]
|
||||
return try! String(data: encoder.encode(self), encoding: .utf8)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: Hashable {}
|
||||
41
Shared/ThirdParty/GenericJSON/Merging.swift
vendored
Normal file
41
Shared/ThirdParty/GenericJSON/Merging.swift
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
extension JSON {
|
||||
|
||||
/// Return a new JSON value by merging two other ones
|
||||
///
|
||||
/// If we call the current JSON value `old` and the incoming JSON value
|
||||
/// `new`, the precise merging rules are:
|
||||
///
|
||||
/// 1. If `old` or `new` are anything but an object, return `new`.
|
||||
/// 2. If both `old` and `new` are objects, create a merged object like this:
|
||||
/// 1. Add keys from `old` not present in `new` (“no change” case).
|
||||
/// 2. Add keys from `new` not present in `old` (“create” case).
|
||||
/// 3. For keys present in both `old` and `new`, apply merge recursively to their values (“update” case).
|
||||
public func merging(with new: JSON) -> JSON {
|
||||
|
||||
// If old or new are anything but an object, return new.
|
||||
guard case .object(let lhs) = self, case .object(let rhs) = new else {
|
||||
return new
|
||||
}
|
||||
|
||||
var merged: [String: JSON] = [:]
|
||||
|
||||
// Add keys from old not present in new (“no change” case).
|
||||
for (key, val) in lhs where rhs[key] == nil {
|
||||
merged[key] = val
|
||||
}
|
||||
|
||||
// Add keys from new not present in old (“create” case).
|
||||
for (key, val) in rhs where lhs[key] == nil {
|
||||
merged[key] = val
|
||||
}
|
||||
|
||||
// For keys present in both old and new, apply merge recursively to their values.
|
||||
for key in lhs.keys where rhs[key] != nil {
|
||||
merged[key] = lhs[key]?.merging(with: rhs[key]!)
|
||||
}
|
||||
|
||||
return JSON.object(merged)
|
||||
}
|
||||
}
|
||||
107
Shared/ThirdParty/GenericJSON/Querying.swift
vendored
Normal file
107
Shared/ThirdParty/GenericJSON/Querying.swift
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
|
||||
public extension JSON {
|
||||
|
||||
/// Return the string value if this is a `.string`, otherwise `nil`
|
||||
var stringValue: String? {
|
||||
if case .string(let value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Return the double value if this is a `.number`, otherwise `nil`
|
||||
var doubleValue: Double? {
|
||||
if case .number(let value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Return the bool value if this is a `.bool`, otherwise `nil`
|
||||
var boolValue: Bool? {
|
||||
if case .bool(let value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Return the object value if this is an `.object`, otherwise `nil`
|
||||
var objectValue: [String: JSON]? {
|
||||
if case .object(let value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Return the array value if this is an `.array`, otherwise `nil`
|
||||
var arrayValue: [JSON]? {
|
||||
if case .array(let value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Return `true` iff this is `.null`
|
||||
var isNull: Bool {
|
||||
if case .null = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// If this is an `.array`, return item at index
|
||||
///
|
||||
/// If this is not an `.array` or the index is out of bounds, returns `nil`.
|
||||
subscript(index: Int) -> JSON? {
|
||||
if case .array(let arr) = self, arr.indices.contains(index) {
|
||||
return arr[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// If this is an `.object`, return item at key
|
||||
subscript(key: String) -> JSON? {
|
||||
if case .object(let dict) = self {
|
||||
return dict[key]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Dynamic member lookup sugar for string subscripts
|
||||
///
|
||||
/// This lets you write `json.foo` instead of `json["foo"]`.
|
||||
subscript(dynamicMember member: String) -> JSON? {
|
||||
return self[member]
|
||||
}
|
||||
|
||||
/// Return the JSON type at the keypath if this is an `.object`, otherwise `nil`
|
||||
///
|
||||
/// This lets you write `json[keyPath: "foo.bar.jar"]`.
|
||||
subscript(keyPath keyPath: String) -> JSON? {
|
||||
return queryKeyPath(keyPath.components(separatedBy: "."))
|
||||
}
|
||||
|
||||
func queryKeyPath<T>(_ path: T) -> JSON? where T: Collection, T.Element == String {
|
||||
|
||||
// Only object values may be subscripted
|
||||
guard case .object(let object) = self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Is the path non-empty?
|
||||
guard let head = path.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a value at the required key?
|
||||
guard let value = object[head] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let tail = path.dropFirst()
|
||||
|
||||
return tail.isEmpty ? value : value.queryKeyPath(tail)
|
||||
}
|
||||
|
||||
}
|
||||
@ -53,6 +53,9 @@ public class Api {
|
||||
}
|
||||
|
||||
let (data, response) = try await self.session.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw self.genError("non-HTTP response received", suggestion: "")
|
||||
}
|
||||
|
||||
// let str = String(data: data, encoding: .utf8)
|
||||
// print("================================")
|
||||
@ -64,11 +67,15 @@ public class Api {
|
||||
// print("================================")
|
||||
|
||||
do {
|
||||
let resp = try JSONDecoder().decode(Response<T>.self, from: data)
|
||||
if resp.success {
|
||||
return resp.data!
|
||||
if data.count > 0 {
|
||||
let resp = try JSONDecoder().decode(Response<T>.self, from: data)
|
||||
if resp.success {
|
||||
return resp.data!
|
||||
} else {
|
||||
throw ApiError(httpStatus: httpResponse.statusCode, message: resp.error, code: resp.errorCode)
|
||||
}
|
||||
} else {
|
||||
throw self.genError(resp.error!, suggestion: "")
|
||||
throw ApiError(httpStatus: httpResponse.statusCode)
|
||||
}
|
||||
} catch let error as Swift.DecodingError {
|
||||
throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription)
|
||||
|
||||
35
Shared/Utils/ApiError.swift
Normal file
35
Shared/Utils/ApiError.swift
Normal file
@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
public enum ApiErrorCode: Int, CustomStringConvertible, Codable {
|
||||
case invalidLoginOrPassword = 0
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidLoginOrPassword: return "Invalid login or password"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiError: LocalizedError {
|
||||
public let httpStatus: Int
|
||||
public let message: String?
|
||||
public let code: ApiErrorCode?
|
||||
|
||||
init(httpStatus: Int, message: String? = nil, code: ApiErrorCode? = nil) {
|
||||
self.httpStatus = httpStatus
|
||||
self.message = message
|
||||
self.code = code
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
if let code = code {
|
||||
return code.description
|
||||
}
|
||||
|
||||
if let message = message {
|
||||
return message
|
||||
}
|
||||
|
||||
return "General http error (status \(self.httpStatus))"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user