Adding API mocking code. API error handling refactoring

This commit is contained in:
Selim Mustafaev 2021-07-25 20:08:04 +03:00
parent d83fc71810
commit 0d261b3452
20 changed files with 958 additions and 93 deletions

View File

@ -36,10 +36,23 @@
7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60726998DCF009B0BC4 /* Alert.swift */; }; 7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60726998DCF009B0BC4 /* Alert.swift */; };
7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60B2699A070009B0BC4 /* MockURLProtocol.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 */; }; 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 */; }; 7A683999269612EA00B2188A /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A683998269612EA00B2188A /* Response.swift */; };
7A68399A269612EA00B2188A /* 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 */; }; 7ACD05D72695C08A00557667 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD05D62695C08A00557667 /* Constants.swift */; };
7ACD05D82695C08A00557667 /* 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 7AF552D82696E5C100578083 /* ApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiTests.swift; sourceTree = "<group>"; };
@ -179,6 +199,7 @@
7A40D5782691C6D7009B0BC4 /* Shared */ = { 7A40D5782691C6D7009B0BC4 /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A971F0B26AD7D27007E527B /* ThirdParty */,
7A40D6002694FF4C009B0BC4 /* Utils */, 7A40D6002694FF4C009B0BC4 /* Utils */,
7A40D5FC2693A90F009B0BC4 /* Extensions */, 7A40D5FC2693A90F009B0BC4 /* Extensions */,
7A40D5EB2693A1C3009B0BC4 /* ViewModels */, 7A40D5EB2693A1C3009B0BC4 /* ViewModels */,
@ -266,8 +287,7 @@
7A40D5F42693A63A009B0BC4 /* AutoCat2Tests */ = { 7A40D5F42693A63A009B0BC4 /* AutoCat2Tests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A503C00269F370A002C1A0D /* Responses */, 7A971F0326AD6EA1007E527B /* Api */,
7A40D60A2699A04F009B0BC4 /* Mocks */,
7A40D5F52693A63A009B0BC4 /* SettingsTests.swift */, 7A40D5F52693A63A009B0BC4 /* SettingsTests.swift */,
7AF552D82696E5C100578083 /* ApiTests.swift */, 7AF552D82696E5C100578083 /* ApiTests.swift */,
); );
@ -287,6 +307,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A40D6012694FF5D009B0BC4 /* Api.swift */, 7A40D6012694FF5D009B0BC4 /* Api.swift */,
7A971F1F26ADC351007E527B /* ApiError.swift */,
7ACD05D62695C08A00557667 /* Constants.swift */, 7ACD05D62695C08A00557667 /* Constants.swift */,
); );
path = Utils; path = Utils;
@ -295,7 +316,8 @@
7A40D60A2699A04F009B0BC4 /* Mocks */ = { 7A40D60A2699A04F009B0BC4 /* Mocks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */, 7A971F0526AD6F2F007E527B /* ApiMethodMock.swift */,
7A971F0726AD7084007E527B /* LoginMethodMock.swift */,
); );
path = Mocks; path = Mocks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -311,13 +333,50 @@
7A503C01269F3797002C1A0D /* Login */ = { 7A503C01269F3797002C1A0D /* Login */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A503C06269F49F4002C1A0D /* login_wrong_credentials.json */,
7A503C04269F494C002C1A0D /* login_invalid_params.json */,
7A503C02269F382F002C1A0D /* login_success.json */, 7A503C02269F382F002C1A0D /* login_success.json */,
); );
path = Login; path = Login;
sourceTree = "<group>"; 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 */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -498,9 +557,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7A503C07269F49F4002C1A0D /* login_wrong_credentials.json in Resources */,
7A503C03269F382F002C1A0D /* login_success.json in Resources */, 7A503C03269F382F002C1A0D /* login_success.json in Resources */,
7A503C05269F494C002C1A0D /* login_invalid_params.json in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -512,13 +569,19 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7A40D5E926938BEC009B0BC4 /* AuthView.swift in Sources */, 7A40D5E926938BEC009B0BC4 /* AuthView.swift in Sources */,
7A971F0D26AD7D4C007E527B /* AnyEncodable.swift in Sources */,
7ACD05D72695C08A00557667 /* Constants.swift in Sources */, 7ACD05D72695C08A00557667 /* Constants.swift in Sources */,
7A971F1926AD8AEB007E527B /* JSON.swift in Sources */,
7A971F1D26AD8AEB007E527B /* Merging.swift in Sources */,
7A40D5E326924B09009B0BC4 /* Settings.swift in Sources */, 7A40D5E326924B09009B0BC4 /* Settings.swift in Sources */,
7AEFAEED26985A3400ED2C85 /* ACProgressView.swift in Sources */, 7AEFAEED26985A3400ED2C85 /* ACProgressView.swift in Sources */,
7A40D5A02691C6D8009B0BC4 /* AutoCat2App.swift in Sources */, 7A40D5A02691C6D8009B0BC4 /* AutoCat2App.swift in Sources */,
7A683999269612EA00B2188A /* Response.swift in Sources */, 7A683999269612EA00B2188A /* Response.swift in Sources */,
7A971F1526AD8AEB007E527B /* Initialization.swift in Sources */,
7A971F1726AD8AEB007E527B /* Querying.swift in Sources */,
7A40D60826998DCF009B0BC4 /* Alert.swift in Sources */, 7A40D60826998DCF009B0BC4 /* Alert.swift in Sources */,
7A40D5A42691C6D8009B0BC4 /* Persistence.swift in Sources */, 7A40D5A42691C6D8009B0BC4 /* Persistence.swift in Sources */,
7A971F2026ADC351007E527B /* ApiError.swift in Sources */,
7A40D5ED2693A1EA009B0BC4 /* AuthVM.swift in Sources */, 7A40D5ED2693A1EA009B0BC4 /* AuthVM.swift in Sources */,
7A40D59E2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */, 7A40D59E2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */,
7A40D5E126924AEC009B0BC4 /* User.swift in Sources */, 7A40D5E126924AEC009B0BC4 /* User.swift in Sources */,
@ -533,13 +596,19 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7A40D5EA26938BEC009B0BC4 /* AuthView.swift in Sources */, 7A40D5EA26938BEC009B0BC4 /* AuthView.swift in Sources */,
7A971F0E26AD7D4C007E527B /* AnyEncodable.swift in Sources */,
7ACD05D82695C08A00557667 /* Constants.swift in Sources */, 7ACD05D82695C08A00557667 /* Constants.swift in Sources */,
7A971F1A26AD8AEB007E527B /* JSON.swift in Sources */,
7A971F1E26AD8AEB007E527B /* Merging.swift in Sources */,
7A40D5A12691C6D8009B0BC4 /* AutoCat2App.swift in Sources */, 7A40D5A12691C6D8009B0BC4 /* AutoCat2App.swift in Sources */,
7AEFAEEE26985A3400ED2C85 /* ACProgressView.swift in Sources */, 7AEFAEEE26985A3400ED2C85 /* ACProgressView.swift in Sources */,
7A40D5A52691C6D8009B0BC4 /* Persistence.swift in Sources */, 7A40D5A52691C6D8009B0BC4 /* Persistence.swift in Sources */,
7A68399A269612EA00B2188A /* Response.swift in Sources */, 7A68399A269612EA00B2188A /* Response.swift in Sources */,
7A971F1626AD8AEB007E527B /* Initialization.swift in Sources */,
7A971F1826AD8AEB007E527B /* Querying.swift in Sources */,
7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */, 7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */,
7A40D5E526924B0C009B0BC4 /* User.swift in Sources */, 7A40D5E526924B0C009B0BC4 /* User.swift in Sources */,
7A971F2126ADC351007E527B /* ApiError.swift in Sources */,
7A40D5EE2693A1EA009B0BC4 /* AuthVM.swift in Sources */, 7A40D5EE2693A1EA009B0BC4 /* AuthVM.swift in Sources */,
7A40D59F2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */, 7A40D59F2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */,
7A40D5A32691C6D8009B0BC4 /* ContentView.swift in Sources */, 7A40D5A32691C6D8009B0BC4 /* ContentView.swift in Sources */,
@ -570,8 +639,11 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */, 7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */,
7A971F0826AD7084007E527B /* LoginMethodMock.swift in Sources */,
7AF552D92696E5C100578083 /* ApiTests.swift in Sources */, 7AF552D92696E5C100578083 /* ApiTests.swift in Sources */,
7A971F0A26AD74FD007E527B /* ApiMethodMockProtocol.swift in Sources */,
7A40D5F62693A63A009B0BC4 /* SettingsTests.swift in Sources */, 7A40D5F62693A63A009B0BC4 /* SettingsTests.swift in Sources */,
7A971F0626AD6F2F007E527B /* ApiMethodMock.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -7,12 +7,12 @@
<key>AutoCat2 (iOS).xcscheme_^#shared#^_</key> <key>AutoCat2 (iOS).xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>0</integer>
</dict> </dict>
<key>AutoCat2 (macOS).xcscheme_^#shared#^_</key> <key>AutoCat2 (macOS).xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict> </dict>
<key>AutoCatCore.xcscheme_^#shared#^_</key> <key>AutoCatCore.xcscheme_^#shared#^_</key>
<dict> <dict>

View 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?)
}

View 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() {
}
}

View 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()
}
}

View 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"))
}
}

View File

@ -4,8 +4,16 @@ import AutoCat2
class ApiTests: XCTestCase { class ApiTests: XCTestCase {
private var api: Api! private var api: Api!
private let testLogin = "test@gmail.com"
private let testPassword = "12345"
override func setUpWithError() throws { 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 let sessionConfig = URLSessionConfiguration.default
sessionConfig.protocolClasses = [MockURLProtocol.self] sessionConfig.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: sessionConfig) let session = URLSession(configuration: sessionConfig)
@ -13,31 +21,25 @@ class ApiTests: XCTestCase {
} }
override func tearDownWithError() throws { 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 { func testLoginSuccess() async throws {
MockURLProtocol.responseType = MockResponseType.loginSuccess let user = try await self.api.login(email: self.testLogin, password: self.testPassword)
let user = try await self.api.login(email: "", password: "")
XCTAssertTrue(!user.token.isEmpty) XCTAssertTrue(!user.token.isEmpty)
} }
func testLoginInvalidParams() async throws { func testLoginInvalidParams() async throws {
MockURLProtocol.responseType = MockResponseType.loginWrongCredentials
do { do {
_ = try await self.api.login(email: "", password: "") _ = try await self.api.login(email: "", password: "")
} catch { } catch let error as ApiError {
XCTAssertTrue(error.code == .invalidLoginOrPassword)
return return
} catch {
XCTFail("Wrong exception type")
} }
XCTFail("Exception expected") 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.
// }
// }
} }

View File

@ -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("")
}
}

View File

@ -1,4 +0,0 @@
{
"success": false,
"error": "Invalid parameters"
}

View File

@ -1,4 +0,0 @@
{
"success": false,
"error": "Incorrect email or password"
}

View File

@ -2,16 +2,22 @@ import CoreLocation
extension NSError { extension NSError {
public var displayMessage: (title: String, body: String) { public var displayMessage: (title: String, body: String) {
if let description = self.userInfo[NSLocalizedDescriptionKey] as? String { // if let description = self.userInfo[NSLocalizedDescriptionKey] as? String {
return (title: "Error", body: description) // return (title: "Error", body: description)
} else if let failure = self.userInfo[NSLocalizedFailureErrorKey] as? String, let reason = self.localizedFailureReason { // } else if let failure = self.userInfo[NSLocalizedFailureErrorKey] as? String, let reason = self.localizedFailureReason {
if let recovery = self.localizedRecoverySuggestion { // if let recovery = self.localizedRecoverySuggestion {
return (title: failure, body: reason + "\n" + recovery) // return (title: failure, body: reason + "\n" + recovery)
} else { // } else {
return (title: failure, body: reason) // return (title: failure, body: reason)
} // }
// } else {
// return (title: "Error", body: "")
// }
if let recovery = self.localizedRecoverySuggestion {
return (title: "Error", body: self.localizedDescription + "\n" + recovery)
} else { } else {
return (title: "Error", body: "") return (title: "Error", body: self.localizedDescription)
} }
} }
} }

View File

@ -4,11 +4,13 @@ class Response<T>: Decodable where T: Decodable {
let success: Bool let success: Bool
let data: T? let data: T?
let error: String? let error: String?
let errorCode: ApiErrorCode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case success case success
case data case data
case error case error
case errorCode
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -17,8 +19,10 @@ class Response<T>: Decodable where T: Decodable {
if success { if success {
data = try container.decode(T.self, forKey: .data) data = try container.decode(T.self, forKey: .data)
error = nil error = nil
errorCode = nil
} else { } else {
error = try container.decode(String.self, forKey: .error) error = try container.decode(String.self, forKey: .error)
errorCode = try container.decodeIfPresent(ApiErrorCode.self, forKey: .errorCode)
data = nil data = nil
} }
} }

273
Shared/ThirdParty/AnyEncodable.swift vendored Normal file
View 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 }))
}
}

View 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)

View 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 {}

View 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)
}
}

View 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)
}
}

View File

@ -53,6 +53,9 @@ public class Api {
} }
let (data, response) = try await self.session.data(for: request) 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) // let str = String(data: data, encoding: .utf8)
// print("================================") // print("================================")
@ -64,11 +67,15 @@ public class Api {
// print("================================") // print("================================")
do { do {
let resp = try JSONDecoder().decode(Response<T>.self, from: data) if data.count > 0 {
if resp.success { let resp = try JSONDecoder().decode(Response<T>.self, from: data)
return resp.data! if resp.success {
return resp.data!
} else {
throw ApiError(httpStatus: httpResponse.statusCode, message: resp.error, code: resp.errorCode)
}
} else { } else {
throw self.genError(resp.error!, suggestion: "") throw ApiError(httpStatus: httpResponse.statusCode)
} }
} catch let error as Swift.DecodingError { } catch let error as Swift.DecodingError {
throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription) throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription)

View 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))"
}
}