├── README.md ├── NetworkSample ├── NetworkSample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Info.plist │ ├── AppDelegate.swift │ ├── Service │ │ ├── RickMortyRepository.swift │ │ ├── RickMortyEndpoint.swift │ │ └── RickMortyCharacterList.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ ├── CustomCell.swift │ └── ViewController.swift ├── NetworkSample.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── xcshareddata │ │ └── xcschemes │ │ │ ├── NetworkSample.xcscheme │ │ │ └── NetworkSampleTests.xcscheme │ └── project.pbxproj ├── .swiftlint.yml └── NetworkSampleTests │ ├── NetworkSampleTests.swift │ └── JSON │ └── character_list_response.json ├── Tests └── NetworkTests │ ├── Mock │ ├── MockError.swift │ ├── ConnectionCheckerMock.swift │ ├── ErrorCheckerMock.swift │ ├── HTTPClientMock.swift │ ├── CreateURLMock.swift │ ├── URLSessionMock.swift │ ├── NetworkReachabilityMock.swift │ ├── NetworkResponseParserMock.swift │ └── JSONMock.swift │ ├── Stub │ ├── DecodableStub.swift │ └── EndpointStub.swift │ ├── Seeds │ ├── HTTPClientSeedTests.swift │ ├── CreateURLSeedTests.swift │ └── ErrorCheckerSeedTests.swift │ └── Tests │ ├── Helpers │ ├── CreateURLTests.swift │ ├── ConnectionCheckerTests.swift │ ├── ErrorCheckerTests.swift │ └── NetworkResponseParserTests.swift │ └── Request │ ├── HTTPClientTests.swift │ └── DefaultNetworkServiceTests.swift ├── Sources ├── Network │ ├── Extension │ │ ├── URLSession.swift │ │ └── JSON.swift │ ├── Model │ │ ├── RequestMethod.swift │ │ └── Endpoint.swift │ ├── Error │ │ └── RequestError.swift │ ├── Request │ │ ├── HTTPClient.swift │ │ └── NetworkService.swift │ └── Helpers │ │ ├── ErrorChecker.swift │ │ ├── CreateURL.swift │ │ ├── NetworkResponseParser.swift │ │ └── ConnectionChecker.swift └── NetworkTestSources │ ├── MockableJSON.swift │ └── Mocks │ └── NetworkServiceMock.swift ├── Package.swift └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # Network 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/MockError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockError.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MockError: Error { 11 | case missingCompletion 12 | case downcastError 13 | } 14 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Stub/DecodableStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecodableStub.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DecodableStub: Codable { 11 | var name: String 12 | var isTest: Bool 13 | } 14 | 15 | extension DecodableStub: Equatable { } 16 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Network/Extension/URLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLSessionProtocol { 11 | func data(for request: URLRequest) async throws -> (Data, URLResponse) 12 | } 13 | 14 | extension URLSession: URLSessionProtocol {} 15 | -------------------------------------------------------------------------------- /Sources/Network/Model/RequestMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestMethod.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum RequestMethod: String { 11 | case get = "GET" 12 | case patch = "PATCH" 13 | case post = "POST" 14 | case put = "PUT" 15 | case delete = "DELETE" 16 | } 17 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "e8625b80c413457b13ea9be75d07f6e9f5830c19", 9 | "version" : "7.7.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Seeds/HTTPClientSeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientSeedTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 24/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HTTPClientSeedTests { 11 | static let someURLRequest: URLRequest = { 12 | var urlRequest = URLRequest(url: URL(string: "www.google.com/test").unsafelyUnwrapped) 13 | urlRequest.httpMethod = "get" 14 | return urlRequest 15 | }() 16 | } 17 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 15 | options connectionOptions: UIScene.ConnectionOptions) { 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/ConnectionCheckerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionCheckerMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class ConnectionCheckerMock: ConnectionCheckerProtocol { 13 | var isConnectedToNetworkCompletion: (() throws -> Void)? 14 | func isConnectedToNetwork() throws { 15 | try isConnectedToNetworkCompletion?() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Network/Error/RequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestError.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum RequestError: Error, Equatable { 11 | case couldNotConnectToServer 12 | case noInternet 13 | case decode 14 | case invalidURL 15 | case noResponse 16 | case unathorized 17 | case unexpectedStatusCode( statusCode: Int, errorMessage: String?) 18 | case unkown 19 | } 20 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/ErrorCheckerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCheckerMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class ErrorCheckerMock: ErrorCheckerProtocol { 13 | var checkErrorCompletion: ((Data, URLResponse) throws -> Void)? 14 | func checkError(data: Data, urlResponse: URLResponse) throws { 15 | try checkErrorCompletion?(data, urlResponse) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/HTTPClientMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class HTTPClientMock: HTTPClient { 13 | var requestCompletion: ((Network.Endpoint) throws -> (Data, URLResponse))? 14 | func request(endpoint: Network.Endpoint) async throws -> (Data, URLResponse) { 15 | return try requestCompletion?(endpoint) ?? (Data(), URLResponse()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/CreateURLMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateURLMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class CreateURLMock: CreateURLProtocol { 13 | 14 | var makeCompletion: ((Network.Endpoint) throws -> URLRequest)? 15 | 16 | func make(endpoint: Network.Endpoint) throws -> URLRequest { 17 | guard let safeMakeCompletion = makeCompletion else { 18 | throw MockError.missingCompletion 19 | } 20 | return try safeMakeCompletion(endpoint) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/URLSessionMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class URLSessionMock: URLSessionProtocol { 13 | 14 | var dataCompletion: ((URLRequest) throws -> (Data, URLResponse))? 15 | 16 | func data(for request: URLRequest) async throws -> (Data, URLResponse) { 17 | guard let safeDataCompletion = dataCompletion else { 18 | throw MockError.missingCompletion 19 | } 20 | return try safeDataCompletion(request) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Seeds/CreateURLSeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateURLSeedTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 24/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CreateURLSeedTests { 11 | static let stubURLRequest: URLRequest = { 12 | var urlRequest = URLRequest(url: URL(string: "https://testhost/testPatch?testQuery=testQuery").unsafelyUnwrapped) 13 | urlRequest.httpMethod = "GET" 14 | urlRequest.allHTTPHeaderFields = ["testHeader": "testHeader"] 15 | urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: ["testBody": "testBody"], options: .prettyPrinted) 16 | return urlRequest 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Network/Model/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Endpoint { 11 | var scheme: String { get } 12 | var host: String { get } 13 | var path: String { get } 14 | var method: RequestMethod { get } 15 | var header: [String: String]? { get } 16 | var queryParameters: [String: String]? { get } 17 | var bodyParameters: [String: Any]? { get } 18 | var port: Int? { get } 19 | } 20 | 21 | public extension Endpoint { 22 | var scheme: String { 23 | return "https" 24 | } 25 | 26 | var port: Int? { 27 | return nil 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/NetworkReachabilityMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkReachabilityMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import SystemConfiguration 9 | 10 | @testable import Network 11 | 12 | final class NetworkReachabilityMock: NetworkReachabilityProtocol { 13 | 14 | var SCNetworkReachabilityGetFlagsCompletion: ((SCNetworkReachability, 15 | UnsafeMutablePointer) -> Bool)? 16 | 17 | func SCNetworkReachabilityGetFlags(_ target: SCNetworkReachability, 18 | _ flags: UnsafeMutablePointer) -> Bool { 19 | return SCNetworkReachabilityGetFlagsCompletion?(target, flags) ?? false 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/Network/Extension/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol JSONDecoderProtocol { 11 | var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy { get set } 12 | var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { get set } 13 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable 14 | } 15 | 16 | extension JSONDecoder: JSONDecoderProtocol {} 17 | 18 | protocol JSONSerializationProtocol { 19 | func jsonObject(with data: Data, options: JSONSerialization.ReadingOptions) throws -> Any 20 | } 21 | 22 | final class JSONSerializationWrappper: JSONSerializationProtocol { 23 | func jsonObject(with data: Data, options: JSONSerialization.ReadingOptions) throws -> Any { 24 | try JSONSerialization.jsonObject(with: data, options: options) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application(_ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | return true 16 | } 17 | 18 | // MARK: UISceneSession Lifecycle 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, 21 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 23 | } 24 | 25 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Network", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | .library( 13 | name: "Network", 14 | targets: ["Network"]), 15 | .library( 16 | name: "NetworkTestSources", 17 | targets: ["NetworkTestSources"]), 18 | ], 19 | dependencies: [ 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Network", 24 | dependencies: []), 25 | .target( 26 | name: "NetworkTestSources", 27 | dependencies: [ 28 | "Network" 29 | ]), 30 | .testTarget( 31 | name: "NetworkTests", 32 | dependencies: [ 33 | "Network", 34 | "NetworkTestSources" 35 | ]), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Stub/EndpointStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EndpointStub.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | enum EndpointStub { 13 | case stub 14 | case invalid 15 | } 16 | 17 | extension EndpointStub: Endpoint { 18 | var host: String { 19 | return "testhost" 20 | } 21 | 22 | var path: String { 23 | switch self { 24 | case .stub: 25 | return "/testPatch" 26 | case .invalid: 27 | return "invalid" 28 | } 29 | } 30 | 31 | var method: Network.RequestMethod { 32 | return .get 33 | } 34 | 35 | var header: [String : String]? { 36 | return ["testHeader": "testHeader"] 37 | } 38 | 39 | var queryParameters: [String : String]? { 40 | return ["testQuery": "testQuery"] 41 | } 42 | 43 | var bodyParameters: [String : Any]? { 44 | return ["testBody": "testBody"] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/NetworkResponseParserMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkResponseParserMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class NetworkResponseParserMock: NetworkResponseParserProtocol { 13 | 14 | var dataToObjectCompletion: ((Data) throws -> Decodable)? 15 | func dataToObject(data: Data, modelType: Model.Type) throws -> Model where Model : Decodable { 16 | 17 | guard let completion = dataToObjectCompletion else { 18 | throw MockError.missingCompletion 19 | } 20 | 21 | guard let model = try completion(data) as? Model else { 22 | throw MockError.downcastError 23 | } 24 | 25 | return model 26 | } 27 | 28 | var dataToDictionaryCompletion: ((Data) throws -> [String: Any])? 29 | func dataToDictionary(data: Data) throws -> [String : Any] { 30 | return try dataToDictionaryCompletion?(data) ?? [:] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/NetworkTestSources/MockableJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mockable.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol MockableProtocol: AnyObject { 11 | var bundle: Bundle { get } 12 | func loadJSON(filename: String, type: T.Type) -> T 13 | } 14 | 15 | public final class Mockable: MockableProtocol { 16 | public var bundle: Bundle { 17 | return Bundle(for: type(of: self)) 18 | } 19 | 20 | public func loadJSON(filename: String, type: T.Type) -> T { 21 | guard let path = bundle.url(forResource: filename, withExtension: "json") else { 22 | fatalError("Failed to load JSON") 23 | } 24 | 25 | do { 26 | let data = try Data(contentsOf: path) 27 | let decodedObject = try JSONDecoder().decode(type, from: data) 28 | 29 | return decodedObject 30 | } catch { 31 | fatalError("Failed to decode loaded JSON") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Service/RickMortyRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RickMortyRepository.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | protocol RickMortysRepositoryProtocol { 12 | func getCharactersList(page: Int) async -> Result 13 | } 14 | 15 | final class RickMortyRepository: RickMortysRepositoryProtocol { 16 | 17 | // MARK: - Properties 18 | 19 | private let networkService: NetworkService 20 | 21 | // MARK: - Init 22 | 23 | init(networkService: NetworkService = DefaultNetworkService()) { 24 | self.networkService = networkService 25 | } 26 | 27 | // MARK: - Methods 28 | 29 | func getCharactersList(page: Int) async -> Result { 30 | return await networkService.request(endpoint: RickMortyEndpoint.charactersList(page: page), 31 | modelType: RickMortyCharacterList.self) 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Network/Request/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClient.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol HTTPClient { 11 | func request(endpoint: Endpoint) async throws -> (Data, URLResponse) 12 | } 13 | 14 | final class DefaultHTTPClient: HTTPClient { 15 | 16 | // MARK: - Properties 17 | 18 | private let createURL: CreateURLProtocol 19 | private let urlSession: URLSessionProtocol 20 | 21 | // MARK: - Init 22 | 23 | init(createURL: CreateURLProtocol = CreateURL(), 24 | urlSession: URLSessionProtocol = URLSession.shared) { 25 | self.createURL = createURL 26 | self.urlSession = urlSession 27 | } 28 | 29 | // MARK: - Methods 30 | 31 | public func request(endpoint: Endpoint) async throws -> (Data, URLResponse) { 32 | let urlRequest = try createURL.make(endpoint: endpoint) 33 | do { 34 | return try await urlSession.data(for: urlRequest) 35 | } catch { 36 | throw RequestError.couldNotConnectToServer 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Mock/JSONMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Network 11 | 12 | final class JSONDecoderMock: JSONDecoderProtocol { 13 | 14 | var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys 15 | var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970 16 | 17 | var decodeCompletion: ((Data) throws -> Decodable)? 18 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { 19 | 20 | guard let safeDecodeCompletion = decodeCompletion else { 21 | throw MockError.missingCompletion 22 | } 23 | guard let object = try safeDecodeCompletion(data) as? T else { 24 | throw MockError.downcastError 25 | } 26 | 27 | return object 28 | } 29 | } 30 | 31 | final class JSONSerializationMock: JSONSerializationProtocol { 32 | var jsonObjectCompletion: ((Data, JSONSerialization.ReadingOptions) throws -> Any)? 33 | func jsonObject(with data: Data, options: JSONSerialization.ReadingOptions) throws -> Any { 34 | guard let safeJsonObjectCompletion = jsonObjectCompletion else { 35 | throw MockError.missingCompletion 36 | } 37 | return try safeJsonObjectCompletion(data, options) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Service/RickMortyEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RickMortyEndpoint.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | enum RickMortyEndpoint { 12 | case charactersList(page: Int) 13 | case characters(id: Int) 14 | case locationsList(page: Int) 15 | } 16 | 17 | extension RickMortyEndpoint: Endpoint { 18 | var host: String { 19 | "rickandmortyapi.com" 20 | } 21 | 22 | var path: String { 23 | switch self { 24 | case .charactersList: 25 | return "/api/character" 26 | case .characters(let id): 27 | return "/api/character/\(id)" 28 | case .locationsList: 29 | return "/api/location" 30 | } 31 | } 32 | 33 | var method: Network.RequestMethod { 34 | .get 35 | } 36 | 37 | var header: [String : String]? { 38 | nil 39 | } 40 | 41 | var queryParameters: [String : String]? { 42 | switch self { 43 | case .charactersList(let page): 44 | return ["page": page.description] 45 | case .characters: 46 | return nil 47 | case .locationsList(let page): 48 | return ["page": page.description] 49 | } 50 | } 51 | 52 | var bodyParameters: [String : Any]? { 53 | return nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Network/Helpers/ErrorChecker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErroCheck.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ErrorCheckerProtocol { 11 | func checkError(data: Data, urlResponse: URLResponse) throws 12 | } 13 | 14 | struct ErrorChecker: ErrorCheckerProtocol { 15 | 16 | // MARK: - Properties 17 | 18 | private let networkResponseParser: NetworkResponseParserProtocol 19 | 20 | // MARK: - Init 21 | 22 | init(networkResponseParser: NetworkResponseParserProtocol = NetworkResponseParser()) { 23 | self.networkResponseParser = networkResponseParser 24 | } 25 | 26 | // MARK: - Properties 27 | 28 | func checkError(data: Data, urlResponse: URLResponse) throws { 29 | guard let respose = urlResponse as? HTTPURLResponse else { throw RequestError.noResponse } 30 | 31 | switch respose.statusCode { 32 | case 200...299: 33 | return 34 | case 401: 35 | throw RequestError.unathorized 36 | default: 37 | let responseObject = try? networkResponseParser.dataToDictionary(data: data) 38 | let errorMessage = responseObject?["message"] as? String 39 | throw RequestError.unexpectedStatusCode(statusCode: respose.statusCode, 40 | errorMessage: errorMessage) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Network/Helpers/CreateURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateURL.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol CreateURLProtocol { 11 | func make(endpoint: Endpoint) throws -> URLRequest 12 | } 13 | 14 | struct CreateURL: CreateURLProtocol { 15 | 16 | // MARK: - Methods 17 | 18 | func make(endpoint: Endpoint) throws -> URLRequest { 19 | var components = URLComponents() 20 | components.scheme = endpoint.scheme 21 | components.host = endpoint.host 22 | components.path = endpoint.path 23 | components.port = endpoint.port 24 | 25 | components.queryItems = endpoint.queryParameters?.compactMap { URLQueryItem(name: $0.key, 26 | value: $0.value)} 27 | 28 | guard let url = components.url else { 29 | throw RequestError.invalidURL 30 | } 31 | 32 | var request = URLRequest(url: url) 33 | request.httpMethod = endpoint.method.rawValue 34 | request.allHTTPHeaderFields = endpoint.header 35 | 36 | if let safeBodyParameters = endpoint.bodyParameters { 37 | request.httpBody = try? JSONSerialization.data(withJSONObject: safeBodyParameters, 38 | options: .prettyPrinted) 39 | } 40 | 41 | return request 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/NetworkTestSources/Mocks/NetworkServiceMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkServiceMock.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | public final class NetworkServiceMock: NetworkService { 12 | 13 | // MARK: - Properties 14 | 15 | var requestCompletion: ((Endpoint) async -> Result)? 16 | var requestWithoutModelCompletion: ((Endpoint) async -> Result<[String: Any], RequestError>)? 17 | 18 | // MARK: - Methods 19 | 20 | public func request(endpoint: Network.Endpoint, 21 | modelType: Model.Type) async -> Result { 22 | guard let safeRequestCompletion = requestCompletion else { 23 | return .failure(.unkown) 24 | } 25 | let result = await safeRequestCompletion(endpoint) 26 | 27 | switch result { 28 | case .success(let success): 29 | guard let model = success as? Model else { return .failure(.unkown)} 30 | return .success(model) 31 | case .failure(let failure): 32 | return .failure(failure) 33 | } 34 | } 35 | 36 | public func request(endpoint: Network.Endpoint) async -> Result<[String : Any], Network.RequestError> { 37 | guard let safeRequestCompletion = requestWithoutModelCompletion else { 38 | return .failure(.unkown) 39 | } 40 | let result = await safeRequestCompletion(endpoint) 41 | 42 | switch result { 43 | case .success(let success): 44 | return .success(success) 45 | case .failure(let failure): 46 | return .failure(failure) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Seeds/ErrorCheckerSeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCheckerSeedTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ErrorCheckerSeedTests { 11 | static let stubEmptyResponse: URLResponse = URLResponse() 12 | 13 | static let stubEmptyData = Data() 14 | 15 | static let expectErrorMessage = "test unexpected status code" 16 | 17 | static let stubUnexpectedStatusCode = 301 18 | 19 | static let stubValidResponse: URLResponse = { 20 | let url = URL(string: "https://www.test.com").unsafelyUnwrapped 21 | let statusCode = 200 22 | let headers = ["Content-Type": "application/json"] 23 | let response = HTTPURLResponse(url: url, statusCode: statusCode, 24 | httpVersion: nil, headerFields: headers) 25 | return response.unsafelyUnwrapped 26 | }() 27 | 28 | static let stubUnathorizedResponse: URLResponse = { 29 | let url = URL(string: "https://www.test.com").unsafelyUnwrapped 30 | let statusCode = 401 31 | let headers = ["Content-Type": "application/json"] 32 | let response = HTTPURLResponse(url: url, statusCode: statusCode, 33 | httpVersion: nil, headerFields: headers) 34 | return response.unsafelyUnwrapped 35 | }() 36 | 37 | static let stubUnexpectedResponse: URLResponse = { 38 | let url = URL(string: "https://www.test.com").unsafelyUnwrapped 39 | let statusCode = Self.stubUnexpectedStatusCode 40 | let headers = ["Content-Type": "application/json"] 41 | let response = HTTPURLResponse(url: url, statusCode: statusCode, 42 | httpVersion: nil, headerFields: headers) 43 | return response.unsafelyUnwrapped 44 | }() 45 | } 46 | -------------------------------------------------------------------------------- /NetworkSample/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - colon 3 | - comma 4 | - control_statement 5 | opt_in_rules: # some rules are only opt-in 6 | - empty_count 7 | # Find all the available rules by running: 8 | # swiftlint rules 9 | included: # paths to include during linting. `--path` is ignored if present. 10 | - ../Sources/ 11 | - ../Tests/ 12 | - ./ 13 | excluded: # paths to ignore during linting. Takes precedence over `included`. 14 | - Carthage 15 | - Pods 16 | - Source/ExcludedFolder 17 | - Source/ExcludedFile.swift 18 | - Source/*/ExcludedFile.swift # Exclude files with a wildcard 19 | analyzer_rules: # Rules run by `swiftlint analyze` (experimental) 20 | - explicit_self 21 | 22 | # configurable rules can be customized from this configuration file 23 | # binary rules can set their severity level 24 | force_cast: warning # implicitly 25 | force_try: 26 | severity: warning # explicitly 27 | # rules that have both warning and error levels, can set just the warning level 28 | # implicitly 29 | line_length: 130 30 | # they can set both implicitly with an array 31 | type_body_length: 32 | - 300 # warning 33 | - 400 # error 34 | # or they can set both explicitly 35 | file_length: 36 | warning: 500 37 | error: 1200 38 | # naming rules can set warnings/errors for min_length and max_length 39 | # additionally they can set excluded names 40 | type_name: 41 | min_length: 4 # only warning 42 | max_length: # warning and error 43 | warning: 40 44 | error: 50 45 | excluded: iPhone # excluded via string 46 | allowed_symbols: ["_"] # these are allowed in type names 47 | identifier_name: 48 | min_length: # only min_length 49 | error: 3 # only error 50 | max_length: 51 | warning: 45 52 | error: 50 53 | excluded: # excluded via string array 54 | - id 55 | - URL 56 | - GlobalAPIKey 57 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) 58 | -------------------------------------------------------------------------------- /Sources/Network/Helpers/NetworkResponseParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkResponseParser.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NetworkResponseParserProtocol { 11 | mutating func dataToObject(data: Data, 12 | modelType: Model.Type) throws -> Model 13 | func dataToDictionary(data: Data) throws -> [String: Any] 14 | } 15 | 16 | struct NetworkResponseParser: NetworkResponseParserProtocol { 17 | 18 | // MARK: - Properties 19 | 20 | private var jsonDecoder: JSONDecoderProtocol 21 | private let jsonSerialization: JSONSerializationProtocol 22 | 23 | // MARK: - Init 24 | 25 | init(jsonDecoder: JSONDecoderProtocol = JSONDecoder(), 26 | jsonSerialization: JSONSerializationProtocol = JSONSerializationWrappper()) { 27 | self.jsonDecoder = jsonDecoder 28 | self.jsonSerialization = jsonSerialization 29 | } 30 | 31 | // MARK: - Methods 32 | 33 | mutating func dataToObject(data: Data, 34 | modelType: Model.Type) throws -> Model where Model : Decodable { 35 | jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 36 | jsonDecoder.dateDecodingStrategy = .secondsSince1970 37 | 38 | do { 39 | return try jsonDecoder.decode(modelType, from: data) 40 | } catch { 41 | throw RequestError.decode 42 | } 43 | } 44 | 45 | func dataToDictionary(data: Data) throws -> [String: Any] { 46 | do { 47 | let json = try jsonSerialization.jsonObject(with: data, 48 | options: .mutableContainers) 49 | guard let dictionaryJson = json as? [String: Any] else { throw RequestError.decode 50 | } 51 | 52 | return dictionaryJson 53 | } catch { 54 | throw RequestError.decode 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Helpers/CreateURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateURLTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 24/05/23. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Network 11 | 12 | final class CreateURLTests: XCTestCase { 13 | 14 | var sut: CreateURL! 15 | 16 | override func setUp() { 17 | sut = CreateURL() 18 | } 19 | 20 | override func tearDown() { 21 | sut = nil 22 | } 23 | 24 | func test_make_WhenCalled_ShouldReturnURLRequestRelatedEndpoint() { 25 | 26 | // Given 27 | var validateURLRequest: URLRequest? 28 | do { 29 | 30 | let expectedResponse = CreateURLSeedTests.stubURLRequest 31 | 32 | // When 33 | validateURLRequest = try sut.make(endpoint: EndpointStub.stub) 34 | 35 | // Then 36 | guard let safeValidateURLRequest = validateURLRequest else { 37 | return XCTFail("Should have urlRequest") 38 | } 39 | XCTAssertEqual(safeValidateURLRequest.url, expectedResponse.url) 40 | XCTAssertEqual(safeValidateURLRequest.httpMethod, expectedResponse.httpMethod) 41 | XCTAssertEqual(safeValidateURLRequest.allHTTPHeaderFields, expectedResponse.allHTTPHeaderFields) 42 | XCTAssertEqual(safeValidateURLRequest.httpBody, expectedResponse.httpBody) 43 | } catch { 44 | XCTFail("It shouldn't throw any errors") 45 | } 46 | } 47 | 48 | func test_make_WhenEndpointURLIsNotValid_ShouldThrowInvalidURLError() { 49 | 50 | // Given 51 | let expectedError = RequestError.invalidURL 52 | 53 | do { 54 | 55 | // When 56 | _ = try sut.make(endpoint: EndpointStub.invalid) 57 | 58 | // Then 59 | XCTFail("Should have thrown error") 60 | } catch let error as RequestError { 61 | XCTAssertEqual(error, expectedError) 62 | } catch { 63 | XCTFail("It should just throw the invalid URL error") 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/Service/RickMortyCharacterList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RickMortyCharacterList.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RickMortyCharacterList: Codable { 11 | let info: Info 12 | let results: [Result] 13 | } 14 | 15 | extension RickMortyCharacterList { 16 | 17 | // MARK: - Info 18 | 19 | struct Info: Codable { 20 | let count, pages: Int 21 | let next: String 22 | let prev: String? 23 | } 24 | 25 | // MARK: - Result 26 | 27 | struct Result: Codable, Equatable { 28 | static func == (lhs: RickMortyCharacterList.Result, 29 | rhs: RickMortyCharacterList.Result) -> Bool { 30 | return ( 31 | lhs.id == rhs.id && 32 | lhs.name == rhs.name) 33 | } 34 | 35 | let id: Int 36 | let name: String 37 | let status: Status 38 | let species: Species 39 | let type: String 40 | let gender: Gender 41 | let origin, location: Location 42 | let image: String 43 | let episode: [String] 44 | let url: String 45 | let created: String 46 | } 47 | 48 | enum Gender: String, Codable { 49 | case female = "Female" 50 | case male = "Male" 51 | case unknown = "unknown" 52 | } 53 | 54 | // MARK: - Location 55 | 56 | struct Location: Codable { 57 | let name: String 58 | let url: String 59 | } 60 | 61 | enum Species: String, Codable { 62 | case alien = "Alien" 63 | case human = "Human" 64 | } 65 | 66 | enum Status: String, Codable { 67 | case alive = "Alive" 68 | case dead = "Dead" 69 | case unknown = "unknown" 70 | } 71 | } 72 | 73 | extension RickMortyCharacterList { 74 | func isSameResult(otherResults: [RickMortyCharacterList.Result]) -> Bool { 75 | if results.count != otherResults.count { 76 | return false 77 | } 78 | 79 | return zip(results, otherResults).filter {$0.0 != $0.1}.isEmpty 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | NetworkSample/Packages/ 41 | NetworkSample/Package.pins 42 | NetworkSample/Package.resolved 43 | .swiftpm/ 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /Sources/Network/Helpers/ConnectionChecker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionChecker.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import SystemConfiguration 9 | 10 | protocol NetworkReachabilityProtocol { 11 | func SCNetworkReachabilityGetFlags(_ target: SCNetworkReachability, 12 | _ flags: UnsafeMutablePointer) -> Bool 13 | } 14 | 15 | final class NetworkReachability: NetworkReachabilityProtocol { 16 | func SCNetworkReachabilityGetFlags(_ target: SCNetworkReachability, 17 | _ flags: UnsafeMutablePointer) -> Bool { 18 | return SystemConfiguration.SCNetworkReachabilityGetFlags(target, flags) 19 | } 20 | 21 | } 22 | 23 | protocol ConnectionCheckerProtocol { 24 | func isConnectedToNetwork() throws 25 | } 26 | 27 | final class ConnectionChecker: ConnectionCheckerProtocol { 28 | 29 | // MARK: - Properties 30 | 31 | let networkReachability: NetworkReachabilityProtocol 32 | 33 | // MARK: - Init 34 | 35 | init(networkReachability: NetworkReachabilityProtocol = NetworkReachability()) { 36 | self.networkReachability = networkReachability 37 | } 38 | 39 | // MARK: - Methods 40 | 41 | func isConnectedToNetwork() throws { 42 | var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, 43 | sin_addr: in_addr(s_addr: 0), 44 | sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) 45 | zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) 46 | zeroAddress.sin_family = sa_family_t(AF_INET) 47 | let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { 48 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in 49 | SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) 50 | } 51 | } 52 | var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) 53 | if networkReachability.SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { 54 | throw RequestError.noInternet 55 | } 56 | // Working for Cellular and WIFI 57 | let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 58 | let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 59 | if !(isReachable && !needsConnection) { 60 | throw RequestError.noInternet 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Helpers/ConnectionCheckerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionCheckerTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import XCTest 9 | import SystemConfiguration 10 | 11 | @testable import Network 12 | 13 | final class ConnectionCheckerTests: XCTestCase { 14 | 15 | var sut: ConnectionChecker! 16 | var networkReachabilityMock: NetworkReachabilityMock! 17 | 18 | override func setUp() { 19 | networkReachabilityMock = NetworkReachabilityMock() 20 | sut = ConnectionChecker(networkReachability: networkReachabilityMock) 21 | } 22 | 23 | override func tearDown() { 24 | networkReachabilityMock = nil 25 | sut = nil 26 | } 27 | 28 | func testIsConnectedToNetwork_WhenNetworkReachabilityFalse_ShouldThrowRequestError() { 29 | 30 | // Given 31 | let expectedError = RequestError.noInternet 32 | 33 | do { 34 | 35 | networkReachabilityMock.SCNetworkReachabilityGetFlagsCompletion = { _, _ in 36 | return false 37 | } 38 | 39 | // When 40 | try sut.isConnectedToNetwork() 41 | 42 | // Then 43 | } catch let error as RequestError { 44 | XCTAssertEqual(error, expectedError) 45 | } catch { 46 | XCTFail("Should throw request error only") 47 | } 48 | } 49 | 50 | func testIsConnectedToNetwork_WhenNetworkReachabilityFlagNotHaveInternet_ShouldThrowRequestError() { 51 | 52 | // Given 53 | let expectedError = RequestError.noInternet 54 | 55 | do { 56 | 57 | networkReachabilityMock.SCNetworkReachabilityGetFlagsCompletion = { _, flags in 58 | flags.pointee = SCNetworkReachabilityFlags(rawValue: 0) 59 | return true 60 | } 61 | 62 | // When 63 | try sut.isConnectedToNetwork() 64 | 65 | // Then 66 | } catch let error as RequestError { 67 | XCTAssertEqual(error, expectedError) 68 | } catch { 69 | XCTFail("Should throw request error only") 70 | } 71 | } 72 | 73 | func testIsConnectedToNetwork_WhenNetworkReachabilityFlagHaveInternet_ShouldNotHaveAnyError() { 74 | 75 | // Given 76 | do { 77 | 78 | networkReachabilityMock.SCNetworkReachabilityGetFlagsCompletion = { _ , flags in 79 | flags.pointee = SCNetworkReachabilityFlags(rawValue: 2) 80 | return true 81 | } 82 | 83 | // When 84 | try sut.isConnectedToNetwork() 85 | 86 | // Then 87 | } catch { 88 | XCTFail("Should not have any error") 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/CustomCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCell.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 22/05/23. 6 | // 7 | 8 | import UIKit 9 | import Kingfisher 10 | 11 | final class CustomCell: UITableViewCell { 12 | 13 | // MARK: - Properties 14 | 15 | static var identifier: String { 16 | String(describing: self) 17 | } 18 | 19 | lazy var icon: UIImageView = { 20 | let image = UIImageView() 21 | image.contentMode = .scaleAspectFit 22 | return image 23 | }() 24 | 25 | lazy var name: UILabel = UILabel() 26 | lazy var species: UILabel = UILabel() 27 | 28 | // MARK: - Override 29 | 30 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 31 | super.init(style: style, reuseIdentifier: reuseIdentifier) 32 | commonSetup() 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | commonSetup() 38 | } 39 | 40 | // MARK: - Methods 41 | 42 | func setup(icon: String, name: String, species: String) { 43 | self.icon.kf.setImage(with: URL(string: icon)) 44 | self.name.text = name 45 | self.species.text = species 46 | } 47 | 48 | // MARK: - Private Methods 49 | 50 | private func commonSetup() { 51 | selectionStyle = .none 52 | setupViewHierarchy() 53 | setupConstraints() 54 | } 55 | 56 | private func setupViewHierarchy() { 57 | contentView.addSubview(icon) 58 | contentView.addSubview(name) 59 | contentView.addSubview(species) 60 | } 61 | 62 | private func setupConstraints() { 63 | icon.translatesAutoresizingMaskIntoConstraints = false 64 | NSLayoutConstraint.activate([ 65 | icon.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), 66 | icon.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 67 | icon.widthAnchor.constraint(equalToConstant: 32), 68 | icon.heightAnchor.constraint(equalToConstant: 32) 69 | ]) 70 | 71 | name.translatesAutoresizingMaskIntoConstraints = false 72 | NSLayoutConstraint.activate([ 73 | name.leftAnchor.constraint(equalTo: icon.rightAnchor, constant: 16), 74 | name.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), 75 | name.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16) 76 | ]) 77 | 78 | species.translatesAutoresizingMaskIntoConstraints = false 79 | NSLayoutConstraint.activate([ 80 | species.leftAnchor.constraint(equalTo: name.leftAnchor), 81 | species.topAnchor.constraint(equalTo: name.bottomAnchor, constant: 8), 82 | species.rightAnchor.constraint(equalTo: name.rightAnchor), 83 | species.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16) 84 | ]) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Network/Request/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol NetworkService { 11 | func request(endpoint: Endpoint, modelType: Model.Type) async -> Result 12 | func request(endpoint: Endpoint) async -> Result<[String: Any], RequestError> 13 | } 14 | 15 | public final class DefaultNetworkService: NetworkService { 16 | 17 | // MARK: - Properties 18 | 19 | private let httpClient: HTTPClient 20 | private let errorChecker: ErrorCheckerProtocol 21 | private let connectionChecker: ConnectionCheckerProtocol 22 | private var networkResponseParser: NetworkResponseParserProtocol 23 | 24 | // MARK: - Init 25 | 26 | public init() { 27 | self.httpClient = DefaultHTTPClient() 28 | self.errorChecker = ErrorChecker() 29 | self.networkResponseParser = NetworkResponseParser() 30 | self.connectionChecker = ConnectionChecker() 31 | } 32 | 33 | init(httpClient: HTTPClient = DefaultHTTPClient(), 34 | errorChecker: ErrorCheckerProtocol = ErrorChecker(), 35 | networkResponseParser: NetworkResponseParserProtocol = NetworkResponseParser(), 36 | connectionChecker: ConnectionCheckerProtocol = ConnectionChecker()) { 37 | self.httpClient = httpClient 38 | self.errorChecker = errorChecker 39 | self.networkResponseParser = networkResponseParser 40 | self.connectionChecker = connectionChecker 41 | } 42 | 43 | // MARK: - Methods 44 | 45 | public func request(endpoint: Endpoint, modelType: Model.Type) async -> Result { 46 | do { 47 | try connectionChecker.isConnectedToNetwork() 48 | let (data, urlResponse) = try await httpClient.request(endpoint: endpoint) 49 | 50 | try errorChecker.checkError(data: data, urlResponse: urlResponse) 51 | 52 | let decoded = try networkResponseParser.dataToObject(data: data, modelType: modelType) 53 | return .success(decoded) 54 | } catch let error as RequestError { 55 | return .failure(error) 56 | } catch { 57 | return .failure(.unkown) 58 | } 59 | } 60 | 61 | public func request(endpoint: Endpoint) async -> Result<[String : Any], RequestError> { 62 | do { 63 | try connectionChecker.isConnectedToNetwork() 64 | let (data, urlResponse) = try await httpClient.request(endpoint: endpoint) 65 | 66 | try errorChecker.checkError(data: data, urlResponse: urlResponse) 67 | 68 | let decoded = try networkResponseParser.dataToDictionary(data: data) 69 | return .success(decoded) 70 | } catch let error as RequestError { 71 | return .failure(error) 72 | } catch { 73 | return .failure(.unkown) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // NetworkSample 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 21/05/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | // MARK: - Properties 13 | 14 | lazy var tableView: UITableView = { 15 | let tableView = UITableView(frame: .zero, 16 | style: .grouped) 17 | return tableView 18 | }() 19 | 20 | var page: Int = 1 21 | @MainActor var dataSource: [RickMortyCharacterList.Result] = [] 22 | 23 | let rickMortyRepository: RickMortysRepositoryProtocol = RickMortyRepository() 24 | 25 | // MARK: - Override 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | setupTableView() 30 | getData() 31 | } 32 | 33 | // MARK: - Private Methods 34 | 35 | private func setupTableView() { 36 | tableView.delegate = self 37 | tableView.dataSource = self 38 | tableView.register(CustomCell.self, forCellReuseIdentifier: CustomCell.identifier) 39 | 40 | view.addSubview(tableView) 41 | tableView.translatesAutoresizingMaskIntoConstraints = false 42 | NSLayoutConstraint.activate([ 43 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor), 44 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 45 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor), 46 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 47 | ]) 48 | } 49 | 50 | private func getData() { 51 | Task(priority: .background) { [weak self] in 52 | let result = await self?.rickMortyRepository.getCharactersList(page: self?.page ?? 1) 53 | switch result { 54 | case .success(let success): 55 | await MainActor.run { 56 | self?.dataSource = success.results 57 | self?.tableView.reloadData() 58 | } 59 | case .failure(let failure): 60 | print(failure.localizedDescription) 61 | case .none: 62 | break 63 | } 64 | } 65 | } 66 | } 67 | 68 | // MARK: - UITableViewDelegate 69 | 70 | extension ViewController: UITableViewDelegate { 71 | 72 | } 73 | 74 | // MARK: - UITableViewDataSource 75 | 76 | extension ViewController: UITableViewDataSource { 77 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 78 | return dataSource.count 79 | } 80 | 81 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 82 | guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as? CustomCell else { 83 | return UITableViewCell() 84 | } 85 | 86 | let source = dataSource[indexPath.row] 87 | cell.setup(icon: source.image, 88 | name: source.name, 89 | species: source.species.rawValue) 90 | 91 | return cell 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/xcshareddata/xcschemes/NetworkSample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/xcshareddata/xcschemes/NetworkSampleTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSampleTests/NetworkSampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkSampleTests.swift 3 | // NetworkSampleTests 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import XCTest 9 | import Network 10 | 11 | @testable import NetworkSample 12 | @testable import NetworkTestSources 13 | 14 | final class RickMortyRepositoryTests: XCTestCase { 15 | 16 | var sut: RickMortyRepository! 17 | var networkServiceMock: NetworkServiceMock! 18 | 19 | override func setUp() { 20 | networkServiceMock = NetworkServiceMock() 21 | sut = RickMortyRepository(networkService: networkServiceMock) 22 | } 23 | 24 | override func tearDown() { 25 | networkServiceMock = nil 26 | sut = nil 27 | } 28 | 29 | // MARK: - Private Methods 30 | 31 | private func isSameEndpoint(endpoint1: Endpoint, endpoint2: Endpoint) -> Bool { 32 | return ( 33 | endpoint1.scheme == endpoint2.scheme && 34 | endpoint1.host == endpoint2.host && 35 | endpoint1.path == endpoint2.path && 36 | endpoint1.method == endpoint2.method 37 | ) 38 | } 39 | 40 | // MARK: - Tests 41 | 42 | func test_getCharactersList_ShouldUseCharactersListEndpoint() async { 43 | 44 | // Given 45 | let expectedPage = 1 46 | let expectedEndpoint: RickMortyEndpoint = RickMortyEndpoint.charactersList(page: expectedPage) 47 | 48 | var validateEndpoint: Endpoint? 49 | 50 | networkServiceMock.requestCompletion = { endpoint in 51 | validateEndpoint = endpoint 52 | return (.failure(.noInternet)) 53 | } 54 | 55 | // When 56 | _ = await sut.getCharactersList(page: expectedPage) 57 | 58 | // Then 59 | guard let safeValidateEndpoint = validateEndpoint else { 60 | return XCTFail("Failed to receive the parameters received in the request") 61 | } 62 | XCTAssertTrue(isSameEndpoint(endpoint1: safeValidateEndpoint, endpoint2: expectedEndpoint)) 63 | } 64 | 65 | func test_getCharactersList_WhenNetworkFailure_ThenCatchTheError() async { 66 | 67 | // Given 68 | let expectedResult = RequestError.noInternet 69 | 70 | networkServiceMock.requestCompletion = { _ in 71 | return (.failure(expectedResult)) 72 | } 73 | 74 | // When 75 | let validateResult = await sut.getCharactersList(page: 1) 76 | 77 | // Then 78 | switch validateResult { 79 | case .success: 80 | XCTFail("should just call the error") 81 | case .failure(let failure): 82 | XCTAssertEqual(failure, expectedResult) 83 | } 84 | } 85 | 86 | func test_getCharactersList_WhenNetworkSuccess_ThenCatchObject() async { 87 | 88 | // Given 89 | let expectedResult = Mockable().loadJSON(filename: "character_list_response", 90 | type: RickMortyCharacterList.self) 91 | 92 | networkServiceMock.requestCompletion = { _ in 93 | return (.success(expectedResult)) 94 | } 95 | 96 | // When 97 | let validateResult = await sut.getCharactersList(page: 1) 98 | 99 | // Then 100 | switch validateResult { 101 | case .success(let success): 102 | XCTAssertTrue(success.isSameResult(otherResults: expectedResult.results)) 103 | case .failure: 104 | XCTFail("should just call the success") 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Helpers/ErrorCheckerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCheckerTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 03/06/23. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Network 11 | 12 | final class ErrorCheckerTests: XCTestCase { 13 | 14 | var sut: ErrorChecker! 15 | var networkResponseParserMock: NetworkResponseParserMock! 16 | 17 | override func setUp() { 18 | networkResponseParserMock = NetworkResponseParserMock() 19 | sut = ErrorChecker(networkResponseParser: networkResponseParserMock) 20 | } 21 | 22 | override func tearDown() { 23 | networkResponseParserMock = nil 24 | sut = nil 25 | } 26 | 27 | func test_checkError_WhenURLResponseNotHaveResponse_ShouldThrowRequestError() { 28 | 29 | // Given 30 | let expectedError = RequestError.noResponse 31 | 32 | do { 33 | 34 | let stubData = Data() 35 | let stubURLResponse = ErrorCheckerSeedTests.stubEmptyResponse 36 | 37 | // When 38 | 39 | try sut.checkError(data: stubData, urlResponse: stubURLResponse) 40 | 41 | // Then 42 | } catch let error as RequestError { 43 | XCTAssertEqual(error, expectedError) 44 | } catch { 45 | XCTFail("should fall on RequestError") 46 | } 47 | } 48 | 49 | func test_checkError_WhenURLResponseHasValidStatusCode_ShouldNotThrowAnyErrors() { 50 | // Given 51 | do { 52 | 53 | let stubData = ErrorCheckerSeedTests.stubEmptyData 54 | let stubURLResponse = ErrorCheckerSeedTests.stubValidResponse 55 | 56 | // When 57 | try sut.checkError(data: stubData, urlResponse: stubURLResponse) 58 | 59 | // Then 60 | } catch { 61 | XCTFail("should not throw any errors") 62 | } 63 | } 64 | 65 | func test_checkError_WhenURLResponseHasStatusCode401_ShouldThrowRequestError() { 66 | // Given 67 | let expectedError = RequestError.unathorized 68 | 69 | do { 70 | 71 | let stubData = ErrorCheckerSeedTests.stubEmptyData 72 | let stubURLResponse = ErrorCheckerSeedTests.stubUnathorizedResponse 73 | 74 | // When 75 | try sut.checkError(data: stubData, urlResponse: stubURLResponse) 76 | 77 | // Then 78 | } catch let error as RequestError { 79 | XCTAssertEqual(error, expectedError) 80 | } catch { 81 | XCTFail("should fall on RequestError") 82 | } 83 | } 84 | 85 | func testCheckError_WhenURLResponseHasStatusCodeUnexpected_ShouldThrowRequestError() { 86 | // Given 87 | let expectedStatusCode = ErrorCheckerSeedTests.stubUnexpectedStatusCode 88 | let expectedErrorMessage = ErrorCheckerSeedTests.expectErrorMessage 89 | 90 | let expectedError = RequestError.unexpectedStatusCode(statusCode: expectedStatusCode, errorMessage: expectedErrorMessage) 91 | 92 | networkResponseParserMock.dataToDictionaryCompletion = { _ in 93 | return ["message": expectedErrorMessage] 94 | } 95 | 96 | do { 97 | 98 | let stubData = ErrorCheckerSeedTests.stubEmptyData 99 | let stubURLResponse = ErrorCheckerSeedTests.stubUnexpectedResponse 100 | 101 | // When 102 | try sut.checkError(data: stubData, urlResponse: stubURLResponse) 103 | 104 | // Then 105 | } catch let error as RequestError { 106 | XCTAssertEqual(error, expectedError) 107 | } catch { 108 | XCTFail("should fall on RequestError") 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Request/HTTPClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 23/05/23. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Network 11 | 12 | final class HTTPClientTests: XCTestCase { 13 | 14 | var sut: DefaultHTTPClient! 15 | var createURLMock: CreateURLMock! 16 | var urlSessionMock: URLSessionMock! 17 | 18 | override func setUp() { 19 | createURLMock = CreateURLMock() 20 | urlSessionMock = URLSessionMock() 21 | sut = DefaultHTTPClient(createURL: createURLMock, 22 | urlSession: urlSessionMock) 23 | } 24 | 25 | override func tearDown() { 26 | createURLMock = nil 27 | urlSessionMock = nil 28 | sut = nil 29 | } 30 | 31 | func test_request_WhenCreateURLRequest_ThenHasSameURLRequestInURLSession() async { 32 | do { 33 | 34 | // Given 35 | let expectedEndpoint = EndpointStub.stub 36 | let expectedURLRequest = HTTPClientSeedTests.someURLRequest 37 | 38 | var validateEndpoint: Network.Endpoint? 39 | var validateURLRequest: URLRequest? 40 | 41 | createURLMock.makeCompletion = { endpoint in 42 | validateEndpoint = endpoint 43 | return expectedURLRequest 44 | } 45 | 46 | urlSessionMock.dataCompletion = { request in 47 | validateURLRequest = request 48 | return (Data(), URLResponse()) 49 | } 50 | 51 | // When 52 | _ = try await sut.request(endpoint: expectedEndpoint) 53 | 54 | // Then 55 | guard let safeValidateEndPoint = validateEndpoint as? EndpointStub, 56 | let safeValidateURLRequest = validateURLRequest else { 57 | return XCTFail("Should have validate Endpoint and URLRequest") 58 | } 59 | 60 | XCTAssertEqual(safeValidateEndPoint, expectedEndpoint) 61 | XCTAssertEqual(safeValidateURLRequest.url, expectedURLRequest.url) 62 | XCTAssertEqual(safeValidateURLRequest.httpMethod, expectedURLRequest.httpMethod) 63 | } catch { 64 | XCTFail("Should not have any error") 65 | } 66 | } 67 | 68 | func test_request_WhenCreateURLThrowError_ThenReturnSameErrorThrow() async { 69 | 70 | // Given 71 | let expectedError = RequestError.invalidURL 72 | 73 | do { 74 | createURLMock.makeCompletion = { _ in 75 | throw RequestError.invalidURL 76 | } 77 | 78 | // When 79 | _ = try await sut.request(endpoint: EndpointStub.stub) 80 | 81 | // Then 82 | } catch let error as RequestError { 83 | XCTAssertEqual(error, expectedError) 84 | } catch { 85 | XCTFail("should have called the couldNotConnectToServer from RequestError") 86 | } 87 | } 88 | 89 | func test_request_WhenURLSessionThrowError_ThenReturnCouldNotConnectServerError() async { 90 | 91 | // Given 92 | let expectedError = RequestError.couldNotConnectToServer 93 | 94 | do { 95 | createURLMock.makeCompletion = { _ in 96 | return HTTPClientSeedTests.someURLRequest 97 | } 98 | 99 | urlSessionMock.dataCompletion = { _ in 100 | throw NSError(domain: "someError", code: 100) 101 | } 102 | 103 | // When 104 | _ = try await sut.request(endpoint: EndpointStub.stub) 105 | 106 | // Then 107 | } catch let error as RequestError { 108 | XCTAssertEqual(error, expectedError) 109 | } catch { 110 | XCTFail("should have called the couldNotConnectToServer from RequestError") 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Helpers/NetworkResponseParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkResponseParserTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Network 11 | 12 | final class NetworkResponseParserTests: XCTestCase { 13 | 14 | var sut: NetworkResponseParser! 15 | var jsonDecoderMock: JSONDecoderMock! 16 | var jsonSerializationMock: JSONSerializationMock! 17 | 18 | override func setUp() { 19 | jsonDecoderMock = JSONDecoderMock() 20 | jsonSerializationMock = JSONSerializationMock() 21 | sut = NetworkResponseParser(jsonDecoder: jsonDecoderMock, 22 | jsonSerialization: jsonSerializationMock) 23 | } 24 | 25 | override func tearDown() { 26 | jsonDecoderMock = nil 27 | jsonSerializationMock = nil 28 | sut = nil 29 | } 30 | 31 | func testDataToObject_WhenDecodeThrowError_ShouldThrowRequestError() { 32 | 33 | // Given 34 | let expectedError = RequestError.decode 35 | 36 | do { 37 | jsonDecoderMock.decodeCompletion = { _ in 38 | throw RequestError.unkown 39 | } 40 | 41 | // When 42 | _ = try sut.dataToObject(data: Data(), modelType: DecodableStub.self) 43 | 44 | // Then 45 | } catch let error as RequestError { 46 | XCTAssertEqual(error, expectedError) 47 | } catch { 48 | XCTFail("should fall on RequestError") 49 | } 50 | } 51 | 52 | func testDataToObject_WhenSuccess_ShouldReturnDecodable() { 53 | 54 | // Given 55 | let expectedDecodable = DecodableStub(name: "test", isTest: true) 56 | let expectedData = "testData".data(using: .utf8).unsafelyUnwrapped 57 | 58 | var validateData: Data? 59 | 60 | do { 61 | jsonDecoderMock.decodeCompletion = { data in 62 | validateData = data 63 | return expectedDecodable 64 | } 65 | 66 | // When 67 | let validateDecodable = try sut.dataToObject(data: expectedData, 68 | modelType: DecodableStub.self) 69 | 70 | // Then 71 | XCTAssertEqual(validateData, expectedData) 72 | XCTAssertEqual(validateDecodable, expectedDecodable) 73 | } catch { 74 | XCTFail("dont should throw error") 75 | } 76 | } 77 | 78 | func testDataToDictionary_WhenSerializationFailure_ShouldThrowRequestError() { 79 | 80 | // Given 81 | let expectedError = RequestError.decode 82 | 83 | do { 84 | 85 | jsonSerializationMock.jsonObjectCompletion = { _, _ in 86 | throw RequestError.unkown 87 | } 88 | 89 | // When 90 | _ = try sut.dataToDictionary(data: Data()) 91 | 92 | // Then 93 | } catch let error as RequestError { 94 | XCTAssertEqual(error, expectedError) 95 | } catch { 96 | XCTFail("should fall on RequestError") 97 | } 98 | } 99 | 100 | func testDataToDictionary_WhenSerializationDontReturnDictionary_ShouldThrowRequestError() { 101 | 102 | // Given 103 | let expectedError = RequestError.decode 104 | 105 | do { 106 | 107 | jsonSerializationMock.jsonObjectCompletion = { _, _ in 108 | return true 109 | } 110 | 111 | // When 112 | _ = try sut.dataToDictionary(data: Data()) 113 | 114 | // Then 115 | } catch let error as RequestError { 116 | XCTAssertEqual(error, expectedError) 117 | } catch { 118 | XCTFail("should fall on RequestError") 119 | } 120 | } 121 | 122 | func testDataToDictionary_WhenSuccess_ShouldReturnDictionary() { 123 | 124 | // Given 125 | let expetedDictionary: [String: Any] = ["test": "testValue"] 126 | let expectedData = "testData".data(using: .utf8).unsafelyUnwrapped 127 | let expectedOptions = JSONSerialization.ReadingOptions.mutableContainers 128 | 129 | var validateData: Data? 130 | var validateOptions: JSONSerialization.ReadingOptions? 131 | 132 | jsonSerializationMock.jsonObjectCompletion = { data, options in 133 | validateData = data 134 | validateOptions = options 135 | 136 | return expetedDictionary 137 | } 138 | 139 | do { 140 | 141 | // When 142 | let result = try sut.dataToDictionary(data: expectedData) 143 | 144 | // Then 145 | XCTAssertEqual(validateData, expectedData) 146 | XCTAssertEqual(validateOptions, expectedOptions) 147 | XCTAssertEqual(result.keys.first, expetedDictionary.keys.first) 148 | XCTAssertEqual(result.values.first as? String, expetedDictionary.values.first as? String) 149 | } catch { 150 | XCTFail("dont should throw error") 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Tests/NetworkTests/Tests/Request/DefaultNetworkServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultNetworkServiceTests.swift 3 | // 4 | // 5 | // Created by Caio Vinicius Pinho Vasconcelos on 04/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | import XCTest 11 | 12 | @testable import Network 13 | 14 | final class DefaultNetworkServiceTests: XCTestCase { 15 | 16 | var sut: DefaultNetworkService! 17 | var httpClientMock: HTTPClientMock! 18 | var errorCheckerMock: ErrorCheckerMock! 19 | var networkResponseParserMock: NetworkResponseParserMock! 20 | var connectionCheckerMock: ConnectionCheckerMock! 21 | 22 | override func setUp() { 23 | httpClientMock = HTTPClientMock() 24 | errorCheckerMock = ErrorCheckerMock() 25 | networkResponseParserMock = NetworkResponseParserMock() 26 | connectionCheckerMock = ConnectionCheckerMock() 27 | sut = DefaultNetworkService(httpClient: httpClientMock, 28 | errorChecker: errorCheckerMock, 29 | networkResponseParser: networkResponseParserMock, 30 | connectionChecker: connectionCheckerMock) 31 | } 32 | 33 | override func tearDown() { 34 | httpClientMock = nil 35 | errorCheckerMock = nil 36 | networkResponseParserMock = nil 37 | connectionCheckerMock = nil 38 | sut = nil 39 | } 40 | 41 | func testRequest_WhenSuccess_ShouldReturnSuccessObject() async { 42 | 43 | // Given 44 | let stubModel = DecodableStub(name: "test", isTest: true) 45 | let stubData = (try? JSONEncoder().encode(stubModel)).unsafelyUnwrapped 46 | 47 | let stubURLResponse = ErrorCheckerSeedTests.stubValidResponse 48 | 49 | var validateEndpoint: Endpoint? 50 | 51 | var validateErrorCheckerData: Data? 52 | var validateErrorCheckerURLResponse: URLResponse? 53 | 54 | var validateNetworkResponseParserData: Data? 55 | 56 | httpClientMock.requestCompletion = { endpoint in 57 | validateEndpoint = endpoint 58 | return (stubData, stubURLResponse) 59 | } 60 | 61 | errorCheckerMock.checkErrorCompletion = { data, urlResponse in 62 | validateErrorCheckerData = data 63 | validateErrorCheckerURLResponse = urlResponse 64 | } 65 | 66 | networkResponseParserMock.dataToObjectCompletion = { data in 67 | validateNetworkResponseParserData = data 68 | return stubModel 69 | } 70 | 71 | // When 72 | let validateResult = await sut.request(endpoint: EndpointStub.stub, 73 | modelType: DecodableStub.self) 74 | 75 | // Then 76 | XCTAssertEqual(validateEndpoint?.host, EndpointStub.stub.host) 77 | XCTAssertEqual(validateEndpoint?.path, EndpointStub.stub.path) 78 | XCTAssertEqual(validateEndpoint?.method, EndpointStub.stub.method) 79 | 80 | XCTAssertEqual(validateErrorCheckerData, stubData) 81 | XCTAssertEqual(validateErrorCheckerURLResponse, stubURLResponse) 82 | 83 | XCTAssertEqual(validateNetworkResponseParserData, stubData) 84 | 85 | guard case let .success(result) = validateResult else { 86 | return XCTFail("Should have success") 87 | } 88 | XCTAssertEqual(result, stubModel) 89 | } 90 | 91 | func testRequest_WhenNetworkIsNotAvaiable_ShouldReturnFailureRequestError() async { 92 | 93 | // Given 94 | let expectedResult = RequestError.noInternet 95 | connectionCheckerMock.isConnectedToNetworkCompletion = { 96 | throw expectedResult 97 | } 98 | 99 | // When 100 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 101 | 102 | // Then 103 | guard case let .failure(error) = validateResult else { 104 | return XCTFail("Should failure test") 105 | } 106 | XCTAssertEqual(error, expectedResult) 107 | } 108 | 109 | func testRequest_WhenNetworkThrowUnkownError_ShouldReturnFailureRequestError() async { 110 | 111 | // Given 112 | let expectedResult = RequestError.unkown 113 | connectionCheckerMock.isConnectedToNetworkCompletion = { 114 | throw NSError(domain: "network", code: 301) 115 | } 116 | 117 | // When 118 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 119 | 120 | // Then 121 | guard case let .failure(error) = validateResult else { 122 | return XCTFail("Should failure test") 123 | } 124 | XCTAssertEqual(error, expectedResult) 125 | } 126 | 127 | func testRequest_WhenHTTPClientThrowError_ShouldReturnFailureRequestError() async { 128 | 129 | // Given 130 | let expectedResult = RequestError.couldNotConnectToServer 131 | httpClientMock.requestCompletion = { _ in 132 | throw expectedResult 133 | } 134 | 135 | // When 136 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 137 | 138 | // Then 139 | guard case let .failure(error) = validateResult else { 140 | return XCTFail("Should failure test") 141 | } 142 | XCTAssertEqual(error, expectedResult) 143 | } 144 | 145 | func testRequest_WhenHTTPClientThrowUnkownError_ShouldReturnFailureRequestError() async { 146 | 147 | // Given 148 | let expectedResult = RequestError.unkown 149 | httpClientMock.requestCompletion = { _ in 150 | throw NSError(domain: "network", code: 301) 151 | } 152 | 153 | // When 154 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 155 | 156 | // Then 157 | guard case let .failure(error) = validateResult else { 158 | return XCTFail("Should failure test") 159 | } 160 | XCTAssertEqual(error, expectedResult) 161 | } 162 | 163 | func testRequest_WhenErrorCheckerThrowError_ShouldReturnFailureRequestError() async { 164 | 165 | // Given 166 | let expectedResult = RequestError.unathorized 167 | errorCheckerMock.checkErrorCompletion = { _, _ in 168 | throw expectedResult 169 | } 170 | 171 | // When 172 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 173 | 174 | // Then 175 | guard case let .failure(error) = validateResult else { 176 | return XCTFail("Should failure test") 177 | } 178 | XCTAssertEqual(error, expectedResult) 179 | } 180 | 181 | func testRequest_WhenNetworkResponseParserThrowError_ShouldReturnFailureRequestError() async { 182 | 183 | // Given 184 | let expectedResult = RequestError.decode 185 | networkResponseParserMock.dataToObjectCompletion = { _ in 186 | throw expectedResult 187 | } 188 | 189 | // When 190 | let validateResult = await sut.request(endpoint: EndpointStub.stub, modelType: DecodableStub.self) 191 | 192 | // Then 193 | guard case let .failure(error) = validateResult else { 194 | return XCTFail("Should failure test") 195 | } 196 | XCTAssertEqual(error, expectedResult) 197 | } 198 | 199 | func testRequestDictionary_WhenSuccess_ShouldReturnSuccessDictionary() async { 200 | 201 | // Given 202 | let stubDictionary = ["test": true] 203 | let stubData = (try? JSONSerialization.data(withJSONObject: stubDictionary)).unsafelyUnwrapped 204 | 205 | let stubURLResponse = ErrorCheckerSeedTests.stubValidResponse 206 | 207 | var validateEndpoint: Endpoint? 208 | 209 | var validateErrorCheckerData: Data? 210 | var validateErrorCheckerURLResponse: URLResponse? 211 | 212 | var validateNetworkResponseParserData: Data? 213 | 214 | httpClientMock.requestCompletion = { endpoint in 215 | validateEndpoint = endpoint 216 | return (stubData, stubURLResponse) 217 | } 218 | 219 | errorCheckerMock.checkErrorCompletion = { data, urlResponse in 220 | validateErrorCheckerData = data 221 | validateErrorCheckerURLResponse = urlResponse 222 | } 223 | 224 | networkResponseParserMock.dataToDictionaryCompletion = { data in 225 | validateNetworkResponseParserData = data 226 | return stubDictionary 227 | } 228 | 229 | // When 230 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 231 | 232 | // Then 233 | XCTAssertEqual(validateEndpoint?.host, EndpointStub.stub.host) 234 | XCTAssertEqual(validateEndpoint?.path, EndpointStub.stub.path) 235 | XCTAssertEqual(validateEndpoint?.method, EndpointStub.stub.method) 236 | 237 | XCTAssertEqual(validateErrorCheckerData, stubData) 238 | XCTAssertEqual(validateErrorCheckerURLResponse, stubURLResponse) 239 | 240 | XCTAssertEqual(validateNetworkResponseParserData, stubData) 241 | 242 | guard case let .success(result) = validateResult else { 243 | return XCTFail("Should have success") 244 | } 245 | XCTAssertEqual(result as? [String: Bool], stubDictionary) 246 | } 247 | 248 | func testRequestDictionary_WhenNetworkIsNotAvaiable_ShouldReturnFailureRequestError() async { 249 | 250 | // Given 251 | let expectedResult = RequestError.noInternet 252 | connectionCheckerMock.isConnectedToNetworkCompletion = { 253 | throw expectedResult 254 | } 255 | 256 | // When 257 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 258 | 259 | // Then 260 | guard case let .failure(error) = validateResult else { 261 | return XCTFail("Should failure test") 262 | } 263 | XCTAssertEqual(error, expectedResult) 264 | } 265 | 266 | func testRequestDictionary_WhenNetworkThrowUnkownError_ShouldReturnFailureRequestError() async { 267 | 268 | // Given 269 | let expectedResult = RequestError.unkown 270 | connectionCheckerMock.isConnectedToNetworkCompletion = { 271 | throw NSError(domain: "network", code: 301) 272 | } 273 | 274 | // When 275 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 276 | 277 | // Then 278 | guard case let .failure(error) = validateResult else { 279 | return XCTFail("Should failure test") 280 | } 281 | XCTAssertEqual(error, expectedResult) 282 | } 283 | 284 | func testRequestDictionary_WhenHTTPClientThrowError_ShouldReturnFailureRequestError() async { 285 | 286 | // Given 287 | let expectedResult = RequestError.couldNotConnectToServer 288 | httpClientMock.requestCompletion = { _ in 289 | throw expectedResult 290 | } 291 | 292 | // When 293 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 294 | 295 | // Then 296 | guard case let .failure(error) = validateResult else { 297 | return XCTFail("Should failure test") 298 | } 299 | XCTAssertEqual(error, expectedResult) 300 | } 301 | 302 | func testRequestDictionary_WhenHTTPClientThrowUnkownError_ShouldReturnFailureRequestError() async { 303 | 304 | // Given 305 | let expectedResult = RequestError.unkown 306 | httpClientMock.requestCompletion = { _ in 307 | throw NSError(domain: "network", code: 301) 308 | } 309 | 310 | // When 311 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 312 | 313 | // Then 314 | guard case let .failure(error) = validateResult else { 315 | return XCTFail("Should failure test") 316 | } 317 | XCTAssertEqual(error, expectedResult) 318 | } 319 | 320 | func testRequestDictionary_WhenErrorCheckerThrowError_ShouldReturnFailureRequestError() async { 321 | 322 | // Given 323 | let expectedResult = RequestError.unathorized 324 | errorCheckerMock.checkErrorCompletion = { _, _ in 325 | throw expectedResult 326 | } 327 | 328 | // When 329 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 330 | 331 | // Then 332 | guard case let .failure(error) = validateResult else { 333 | return XCTFail("Should failure test") 334 | } 335 | XCTAssertEqual(error, expectedResult) 336 | } 337 | 338 | func testRequestDictionary_WhenNetworkResponseParserThrowError_ShouldReturnFailureRequestError() async { 339 | 340 | // Given 341 | let expectedResult = RequestError.decode 342 | networkResponseParserMock.dataToDictionaryCompletion = { _ in 343 | throw expectedResult 344 | } 345 | 346 | // When 347 | let validateResult = await sut.request(endpoint: EndpointStub.stub) 348 | 349 | // Then 350 | guard case let .failure(error) = validateResult else { 351 | return XCTFail("Should failure test") 352 | } 353 | XCTAssertEqual(error, expectedResult) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DD43C6BF2A1ABB8400C1EACD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD43C6BE2A1ABB8400C1EACD /* AppDelegate.swift */; }; 11 | DD43C6C12A1ABB8400C1EACD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD43C6C02A1ABB8400C1EACD /* SceneDelegate.swift */; }; 12 | DD43C6C32A1ABB8400C1EACD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD43C6C22A1ABB8400C1EACD /* ViewController.swift */; }; 13 | DD43C6C62A1ABB8400C1EACD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD43C6C42A1ABB8400C1EACD /* Main.storyboard */; }; 14 | DD43C6C82A1ABB8600C1EACD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD43C6C72A1ABB8600C1EACD /* Assets.xcassets */; }; 15 | DD43C6CB2A1ABB8600C1EACD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD43C6C92A1ABB8600C1EACD /* LaunchScreen.storyboard */; }; 16 | DD68F3D52A1ADDCD00FEED31 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = DD68F3D42A1ADDCD00FEED31 /* Network */; }; 17 | DD92611F2A1C4B7700A8D4B8 /* RickMortyEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD92611E2A1C4B7700A8D4B8 /* RickMortyEndpoint.swift */; }; 18 | DD9261212A1C4D7500A8D4B8 /* RickMortyRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9261202A1C4D7500A8D4B8 /* RickMortyRepository.swift */; }; 19 | DD9261232A1C505200A8D4B8 /* RickMortyCharacterList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9261222A1C505200A8D4B8 /* RickMortyCharacterList.swift */; }; 20 | DD9261252A1C58B700A8D4B8 /* CustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9261242A1C58B700A8D4B8 /* CustomCell.swift */; }; 21 | DD9261282A1C5B8A00A8D4B8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DD9261272A1C5B8A00A8D4B8 /* Kingfisher */; }; 22 | DDB523DA2A1C7C1B00A093AE /* NetworkSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB523D92A1C7C1B00A093AE /* NetworkSampleTests.swift */; }; 23 | DDB523E12A1C7FD300A093AE /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = DDB523E02A1C7FD300A093AE /* Network */; }; 24 | DDB523E32A1C7FD900A093AE /* NetworkTestSources in Frameworks */ = {isa = PBXBuildFile; productRef = DDB523E22A1C7FD900A093AE /* NetworkTestSources */; }; 25 | DDBE87C62A1DA0D300C98D58 /* character_list_response.json in Resources */ = {isa = PBXBuildFile; fileRef = DDBE87C52A1DA0D300C98D58 /* character_list_response.json */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | DDB523DB2A1C7C1B00A093AE /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = DD43C6B32A1ABB8400C1EACD /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = DD43C6BA2A1ABB8400C1EACD; 34 | remoteInfo = NetworkSample; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | DD43C6BB2A1ABB8400C1EACD /* NetworkSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetworkSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | DD43C6BE2A1ABB8400C1EACD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | DD43C6C02A1ABB8400C1EACD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 42 | DD43C6C22A1ABB8400C1EACD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 43 | DD43C6C52A1ABB8400C1EACD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | DD43C6C72A1ABB8600C1EACD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | DD43C6CA2A1ABB8600C1EACD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | DD43C6CC2A1ABB8600C1EACD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | DD698DD52A1AC73B007B721A /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = ..; sourceTree = ""; }; 48 | DD92611E2A1C4B7700A8D4B8 /* RickMortyEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickMortyEndpoint.swift; sourceTree = ""; }; 49 | DD9261202A1C4D7500A8D4B8 /* RickMortyRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickMortyRepository.swift; sourceTree = ""; }; 50 | DD9261222A1C505200A8D4B8 /* RickMortyCharacterList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickMortyCharacterList.swift; sourceTree = ""; }; 51 | DD9261242A1C58B700A8D4B8 /* CustomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCell.swift; sourceTree = ""; }; 52 | DDB523D72A1C7C1B00A093AE /* NetworkSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | DDB523D92A1C7C1B00A093AE /* NetworkSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSampleTests.swift; sourceTree = ""; }; 54 | DDBE87C52A1DA0D300C98D58 /* character_list_response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = character_list_response.json; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | DD43C6B82A1ABB8400C1EACD /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | DD9261282A1C5B8A00A8D4B8 /* Kingfisher in Frameworks */, 63 | DD68F3D52A1ADDCD00FEED31 /* Network in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | DDB523D42A1C7C1B00A093AE /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | DDB523E32A1C7FD900A093AE /* NetworkTestSources in Frameworks */, 72 | DDB523E12A1C7FD300A093AE /* Network in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | DD43C6B22A1ABB8400C1EACD = { 80 | isa = PBXGroup; 81 | children = ( 82 | DD698DD52A1AC73B007B721A /* Network */, 83 | DD43C6BD2A1ABB8400C1EACD /* NetworkSample */, 84 | DDB523D82A1C7C1B00A093AE /* NetworkSampleTests */, 85 | DD43C6BC2A1ABB8400C1EACD /* Products */, 86 | DD68F3D32A1ADDCD00FEED31 /* Frameworks */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | DD43C6BC2A1ABB8400C1EACD /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | DD43C6BB2A1ABB8400C1EACD /* NetworkSample.app */, 94 | DDB523D72A1C7C1B00A093AE /* NetworkSampleTests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | DD43C6BD2A1ABB8400C1EACD /* NetworkSample */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | DD92611D2A1C4B5800A8D4B8 /* Service */, 103 | DD43C6BE2A1ABB8400C1EACD /* AppDelegate.swift */, 104 | DD43C6C02A1ABB8400C1EACD /* SceneDelegate.swift */, 105 | DD43C6C22A1ABB8400C1EACD /* ViewController.swift */, 106 | DD9261242A1C58B700A8D4B8 /* CustomCell.swift */, 107 | DD43C6C42A1ABB8400C1EACD /* Main.storyboard */, 108 | DD43C6C72A1ABB8600C1EACD /* Assets.xcassets */, 109 | DD43C6C92A1ABB8600C1EACD /* LaunchScreen.storyboard */, 110 | DD43C6CC2A1ABB8600C1EACD /* Info.plist */, 111 | ); 112 | path = NetworkSample; 113 | sourceTree = ""; 114 | }; 115 | DD68F3D32A1ADDCD00FEED31 /* Frameworks */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | ); 119 | name = Frameworks; 120 | sourceTree = ""; 121 | }; 122 | DD92611D2A1C4B5800A8D4B8 /* Service */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | DD9261202A1C4D7500A8D4B8 /* RickMortyRepository.swift */, 126 | DD92611E2A1C4B7700A8D4B8 /* RickMortyEndpoint.swift */, 127 | DD9261222A1C505200A8D4B8 /* RickMortyCharacterList.swift */, 128 | ); 129 | path = Service; 130 | sourceTree = ""; 131 | }; 132 | DDB523D82A1C7C1B00A093AE /* NetworkSampleTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | DDBE87C42A1DA09900C98D58 /* JSON */, 136 | DDB523D92A1C7C1B00A093AE /* NetworkSampleTests.swift */, 137 | ); 138 | path = NetworkSampleTests; 139 | sourceTree = ""; 140 | }; 141 | DDBE87C42A1DA09900C98D58 /* JSON */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | DDBE87C52A1DA0D300C98D58 /* character_list_response.json */, 145 | ); 146 | path = JSON; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | DD43C6BA2A1ABB8400C1EACD /* NetworkSample */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = DD43C6CF2A1ABB8600C1EACD /* Build configuration list for PBXNativeTarget "NetworkSample" */; 155 | buildPhases = ( 156 | DD43C6B72A1ABB8400C1EACD /* Sources */, 157 | DD43C6B82A1ABB8400C1EACD /* Frameworks */, 158 | DD43C6B92A1ABB8400C1EACD /* Resources */, 159 | DD14AD4B2A2E13B000A1BC71 /* Run Lint */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | ); 165 | name = NetworkSample; 166 | packageProductDependencies = ( 167 | DD68F3D42A1ADDCD00FEED31 /* Network */, 168 | DD9261272A1C5B8A00A8D4B8 /* Kingfisher */, 169 | ); 170 | productName = NetworkSample; 171 | productReference = DD43C6BB2A1ABB8400C1EACD /* NetworkSample.app */; 172 | productType = "com.apple.product-type.application"; 173 | }; 174 | DDB523D62A1C7C1B00A093AE /* NetworkSampleTests */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = DDB523DF2A1C7C1B00A093AE /* Build configuration list for PBXNativeTarget "NetworkSampleTests" */; 177 | buildPhases = ( 178 | DDB523D32A1C7C1B00A093AE /* Sources */, 179 | DDB523D42A1C7C1B00A093AE /* Frameworks */, 180 | DDB523D52A1C7C1B00A093AE /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | DDB523DC2A1C7C1B00A093AE /* PBXTargetDependency */, 186 | ); 187 | name = NetworkSampleTests; 188 | packageProductDependencies = ( 189 | DDB523E02A1C7FD300A093AE /* Network */, 190 | DDB523E22A1C7FD900A093AE /* NetworkTestSources */, 191 | ); 192 | productName = NetworkSampleTests; 193 | productReference = DDB523D72A1C7C1B00A093AE /* NetworkSampleTests.xctest */; 194 | productType = "com.apple.product-type.bundle.unit-test"; 195 | }; 196 | /* End PBXNativeTarget section */ 197 | 198 | /* Begin PBXProject section */ 199 | DD43C6B32A1ABB8400C1EACD /* Project object */ = { 200 | isa = PBXProject; 201 | attributes = { 202 | BuildIndependentTargetsInParallel = 1; 203 | LastSwiftUpdateCheck = 1420; 204 | LastUpgradeCheck = 1420; 205 | TargetAttributes = { 206 | DD43C6BA2A1ABB8400C1EACD = { 207 | CreatedOnToolsVersion = 14.2; 208 | }; 209 | DDB523D62A1C7C1B00A093AE = { 210 | CreatedOnToolsVersion = 14.2; 211 | TestTargetID = DD43C6BA2A1ABB8400C1EACD; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = DD43C6B62A1ABB8400C1EACD /* Build configuration list for PBXProject "NetworkSample" */; 216 | compatibilityVersion = "Xcode 14.0"; 217 | developmentRegion = en; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = DD43C6B22A1ABB8400C1EACD; 224 | packageReferences = ( 225 | DD9261262A1C5B8A00A8D4B8 /* XCRemoteSwiftPackageReference "Kingfisher" */, 226 | ); 227 | productRefGroup = DD43C6BC2A1ABB8400C1EACD /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | DD43C6BA2A1ABB8400C1EACD /* NetworkSample */, 232 | DDB523D62A1C7C1B00A093AE /* NetworkSampleTests */, 233 | ); 234 | }; 235 | /* End PBXProject section */ 236 | 237 | /* Begin PBXResourcesBuildPhase section */ 238 | DD43C6B92A1ABB8400C1EACD /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | DD43C6CB2A1ABB8600C1EACD /* LaunchScreen.storyboard in Resources */, 243 | DD43C6C82A1ABB8600C1EACD /* Assets.xcassets in Resources */, 244 | DD43C6C62A1ABB8400C1EACD /* Main.storyboard in Resources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | DDB523D52A1C7C1B00A093AE /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | DDBE87C62A1DA0D300C98D58 /* character_list_response.json in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXShellScriptBuildPhase section */ 259 | DD14AD4B2A2E13B000A1BC71 /* Run Lint */ = { 260 | isa = PBXShellScriptBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | inputFileListPaths = ( 265 | ); 266 | inputPaths = ( 267 | ); 268 | name = "Run Lint"; 269 | outputFileListPaths = ( 270 | ); 271 | outputPaths = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | shellPath = /bin/sh; 275 | shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n if which swiftlint >/dev/null; then\n swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; 276 | }; 277 | /* End PBXShellScriptBuildPhase section */ 278 | 279 | /* Begin PBXSourcesBuildPhase section */ 280 | DD43C6B72A1ABB8400C1EACD /* Sources */ = { 281 | isa = PBXSourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | DD43C6C32A1ABB8400C1EACD /* ViewController.swift in Sources */, 285 | DD43C6BF2A1ABB8400C1EACD /* AppDelegate.swift in Sources */, 286 | DD9261232A1C505200A8D4B8 /* RickMortyCharacterList.swift in Sources */, 287 | DD92611F2A1C4B7700A8D4B8 /* RickMortyEndpoint.swift in Sources */, 288 | DD43C6C12A1ABB8400C1EACD /* SceneDelegate.swift in Sources */, 289 | DD9261252A1C58B700A8D4B8 /* CustomCell.swift in Sources */, 290 | DD9261212A1C4D7500A8D4B8 /* RickMortyRepository.swift in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | DDB523D32A1C7C1B00A093AE /* Sources */ = { 295 | isa = PBXSourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | DDB523DA2A1C7C1B00A093AE /* NetworkSampleTests.swift in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXTargetDependency section */ 305 | DDB523DC2A1C7C1B00A093AE /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = DD43C6BA2A1ABB8400C1EACD /* NetworkSample */; 308 | targetProxy = DDB523DB2A1C7C1B00A093AE /* PBXContainerItemProxy */; 309 | }; 310 | /* End PBXTargetDependency section */ 311 | 312 | /* Begin PBXVariantGroup section */ 313 | DD43C6C42A1ABB8400C1EACD /* Main.storyboard */ = { 314 | isa = PBXVariantGroup; 315 | children = ( 316 | DD43C6C52A1ABB8400C1EACD /* Base */, 317 | ); 318 | name = Main.storyboard; 319 | sourceTree = ""; 320 | }; 321 | DD43C6C92A1ABB8600C1EACD /* LaunchScreen.storyboard */ = { 322 | isa = PBXVariantGroup; 323 | children = ( 324 | DD43C6CA2A1ABB8600C1EACD /* Base */, 325 | ); 326 | name = LaunchScreen.storyboard; 327 | sourceTree = ""; 328 | }; 329 | /* End PBXVariantGroup section */ 330 | 331 | /* Begin XCBuildConfiguration section */ 332 | DD43C6CD2A1ABB8600C1EACD /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_ANALYZER_NONNULL = YES; 337 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 338 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 339 | CLANG_ENABLE_MODULES = YES; 340 | CLANG_ENABLE_OBJC_ARC = YES; 341 | CLANG_ENABLE_OBJC_WEAK = YES; 342 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 343 | CLANG_WARN_BOOL_CONVERSION = YES; 344 | CLANG_WARN_COMMA = YES; 345 | CLANG_WARN_CONSTANT_CONVERSION = YES; 346 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 347 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 348 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 349 | CLANG_WARN_EMPTY_BODY = YES; 350 | CLANG_WARN_ENUM_CONVERSION = YES; 351 | CLANG_WARN_INFINITE_RECURSION = YES; 352 | CLANG_WARN_INT_CONVERSION = YES; 353 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 355 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 356 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 357 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 358 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 359 | CLANG_WARN_STRICT_PROTOTYPES = YES; 360 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 361 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 362 | CLANG_WARN_UNREACHABLE_CODE = YES; 363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 364 | COPY_PHASE_STRIP = NO; 365 | DEBUG_INFORMATION_FORMAT = dwarf; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | ENABLE_TESTABILITY = YES; 368 | GCC_C_LANGUAGE_STANDARD = gnu11; 369 | GCC_DYNAMIC_NO_PIC = NO; 370 | GCC_NO_COMMON_BLOCKS = YES; 371 | GCC_OPTIMIZATION_LEVEL = 0; 372 | GCC_PREPROCESSOR_DEFINITIONS = ( 373 | "DEBUG=1", 374 | "$(inherited)", 375 | ); 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 383 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 384 | MTL_FAST_MATH = YES; 385 | ONLY_ACTIVE_ARCH = YES; 386 | SDKROOT = iphoneos; 387 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 388 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 389 | SWIFT_STRICT_CONCURRENCY = complete; 390 | SWIFT_VERSION = 5.0; 391 | }; 392 | name = Debug; 393 | }; 394 | DD43C6CE2A1ABB8600C1EACD /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | CLANG_ANALYZER_NONNULL = YES; 399 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_ENABLE_OBJC_WEAK = YES; 404 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 405 | CLANG_WARN_BOOL_CONVERSION = YES; 406 | CLANG_WARN_COMMA = YES; 407 | CLANG_WARN_CONSTANT_CONVERSION = YES; 408 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 411 | CLANG_WARN_EMPTY_BODY = YES; 412 | CLANG_WARN_ENUM_CONVERSION = YES; 413 | CLANG_WARN_INFINITE_RECURSION = YES; 414 | CLANG_WARN_INT_CONVERSION = YES; 415 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 417 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 418 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 419 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 420 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 421 | CLANG_WARN_STRICT_PROTOTYPES = YES; 422 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 423 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 424 | CLANG_WARN_UNREACHABLE_CODE = YES; 425 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 426 | COPY_PHASE_STRIP = NO; 427 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 428 | ENABLE_NS_ASSERTIONS = NO; 429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 430 | GCC_C_LANGUAGE_STANDARD = gnu11; 431 | GCC_NO_COMMON_BLOCKS = YES; 432 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 433 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 434 | GCC_WARN_UNDECLARED_SELECTOR = YES; 435 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 436 | GCC_WARN_UNUSED_FUNCTION = YES; 437 | GCC_WARN_UNUSED_VARIABLE = YES; 438 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 439 | MTL_ENABLE_DEBUG_INFO = NO; 440 | MTL_FAST_MATH = YES; 441 | SDKROOT = iphoneos; 442 | SWIFT_COMPILATION_MODE = wholemodule; 443 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 444 | SWIFT_STRICT_CONCURRENCY = complete; 445 | SWIFT_VERSION = 5.0; 446 | VALIDATE_PRODUCT = YES; 447 | }; 448 | name = Release; 449 | }; 450 | DD43C6D02A1ABB8600C1EACD /* Debug */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 454 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 455 | CODE_SIGN_STYLE = Automatic; 456 | CURRENT_PROJECT_VERSION = 1; 457 | DEVELOPMENT_TEAM = PP576GT3WX; 458 | GENERATE_INFOPLIST_FILE = YES; 459 | INFOPLIST_FILE = NetworkSample/Info.plist; 460 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 461 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 462 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 463 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 464 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 465 | LD_RUNPATH_SEARCH_PATHS = ( 466 | "$(inherited)", 467 | "@executable_path/Frameworks", 468 | ); 469 | MARKETING_VERSION = 1.0; 470 | PRODUCT_BUNDLE_IDENTIFIER = com.akira.NetworkSample; 471 | PRODUCT_NAME = "$(TARGET_NAME)"; 472 | SWIFT_EMIT_LOC_STRINGS = YES; 473 | SWIFT_STRICT_CONCURRENCY = complete; 474 | SWIFT_VERSION = 5.0; 475 | TARGETED_DEVICE_FAMILY = "1,2"; 476 | }; 477 | name = Debug; 478 | }; 479 | DD43C6D12A1ABB8600C1EACD /* Release */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 483 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 484 | CODE_SIGN_STYLE = Automatic; 485 | CURRENT_PROJECT_VERSION = 1; 486 | DEVELOPMENT_TEAM = PP576GT3WX; 487 | GENERATE_INFOPLIST_FILE = YES; 488 | INFOPLIST_FILE = NetworkSample/Info.plist; 489 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 490 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 491 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 492 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 493 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 494 | LD_RUNPATH_SEARCH_PATHS = ( 495 | "$(inherited)", 496 | "@executable_path/Frameworks", 497 | ); 498 | MARKETING_VERSION = 1.0; 499 | PRODUCT_BUNDLE_IDENTIFIER = com.akira.NetworkSample; 500 | PRODUCT_NAME = "$(TARGET_NAME)"; 501 | SWIFT_EMIT_LOC_STRINGS = YES; 502 | SWIFT_STRICT_CONCURRENCY = complete; 503 | SWIFT_VERSION = 5.0; 504 | TARGETED_DEVICE_FAMILY = "1,2"; 505 | }; 506 | name = Release; 507 | }; 508 | DDB523DD2A1C7C1B00A093AE /* Debug */ = { 509 | isa = XCBuildConfiguration; 510 | buildSettings = { 511 | BUNDLE_LOADER = "$(TEST_HOST)"; 512 | CODE_SIGN_STYLE = Automatic; 513 | CURRENT_PROJECT_VERSION = 1; 514 | DEVELOPMENT_TEAM = PP576GT3WX; 515 | GENERATE_INFOPLIST_FILE = YES; 516 | MARKETING_VERSION = 1.0; 517 | PRODUCT_BUNDLE_IDENTIFIER = com.akira.NetworkSampleTests; 518 | PRODUCT_NAME = "$(TARGET_NAME)"; 519 | SWIFT_EMIT_LOC_STRINGS = NO; 520 | SWIFT_STRICT_CONCURRENCY = minimal; 521 | SWIFT_VERSION = 5.0; 522 | TARGETED_DEVICE_FAMILY = "1,2"; 523 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetworkSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NetworkSample"; 524 | }; 525 | name = Debug; 526 | }; 527 | DDB523DE2A1C7C1B00A093AE /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | BUNDLE_LOADER = "$(TEST_HOST)"; 531 | CODE_SIGN_STYLE = Automatic; 532 | CURRENT_PROJECT_VERSION = 1; 533 | DEVELOPMENT_TEAM = PP576GT3WX; 534 | GENERATE_INFOPLIST_FILE = YES; 535 | MARKETING_VERSION = 1.0; 536 | PRODUCT_BUNDLE_IDENTIFIER = com.akira.NetworkSampleTests; 537 | PRODUCT_NAME = "$(TARGET_NAME)"; 538 | SWIFT_EMIT_LOC_STRINGS = NO; 539 | SWIFT_STRICT_CONCURRENCY = minimal; 540 | SWIFT_VERSION = 5.0; 541 | TARGETED_DEVICE_FAMILY = "1,2"; 542 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetworkSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NetworkSample"; 543 | }; 544 | name = Release; 545 | }; 546 | /* End XCBuildConfiguration section */ 547 | 548 | /* Begin XCConfigurationList section */ 549 | DD43C6B62A1ABB8400C1EACD /* Build configuration list for PBXProject "NetworkSample" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | DD43C6CD2A1ABB8600C1EACD /* Debug */, 553 | DD43C6CE2A1ABB8600C1EACD /* Release */, 554 | ); 555 | defaultConfigurationIsVisible = 0; 556 | defaultConfigurationName = Release; 557 | }; 558 | DD43C6CF2A1ABB8600C1EACD /* Build configuration list for PBXNativeTarget "NetworkSample" */ = { 559 | isa = XCConfigurationList; 560 | buildConfigurations = ( 561 | DD43C6D02A1ABB8600C1EACD /* Debug */, 562 | DD43C6D12A1ABB8600C1EACD /* Release */, 563 | ); 564 | defaultConfigurationIsVisible = 0; 565 | defaultConfigurationName = Release; 566 | }; 567 | DDB523DF2A1C7C1B00A093AE /* Build configuration list for PBXNativeTarget "NetworkSampleTests" */ = { 568 | isa = XCConfigurationList; 569 | buildConfigurations = ( 570 | DDB523DD2A1C7C1B00A093AE /* Debug */, 571 | DDB523DE2A1C7C1B00A093AE /* Release */, 572 | ); 573 | defaultConfigurationIsVisible = 0; 574 | defaultConfigurationName = Release; 575 | }; 576 | /* End XCConfigurationList section */ 577 | 578 | /* Begin XCRemoteSwiftPackageReference section */ 579 | DD9261262A1C5B8A00A8D4B8 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 580 | isa = XCRemoteSwiftPackageReference; 581 | repositoryURL = "https://github.com/onevcat/Kingfisher.git"; 582 | requirement = { 583 | kind = upToNextMajorVersion; 584 | minimumVersion = 7.0.0; 585 | }; 586 | }; 587 | /* End XCRemoteSwiftPackageReference section */ 588 | 589 | /* Begin XCSwiftPackageProductDependency section */ 590 | DD68F3D42A1ADDCD00FEED31 /* Network */ = { 591 | isa = XCSwiftPackageProductDependency; 592 | productName = Network; 593 | }; 594 | DD9261272A1C5B8A00A8D4B8 /* Kingfisher */ = { 595 | isa = XCSwiftPackageProductDependency; 596 | package = DD9261262A1C5B8A00A8D4B8 /* XCRemoteSwiftPackageReference "Kingfisher" */; 597 | productName = Kingfisher; 598 | }; 599 | DDB523E02A1C7FD300A093AE /* Network */ = { 600 | isa = XCSwiftPackageProductDependency; 601 | productName = Network; 602 | }; 603 | DDB523E22A1C7FD900A093AE /* NetworkTestSources */ = { 604 | isa = XCSwiftPackageProductDependency; 605 | productName = NetworkTestSources; 606 | }; 607 | /* End XCSwiftPackageProductDependency section */ 608 | }; 609 | rootObject = DD43C6B32A1ABB8400C1EACD /* Project object */; 610 | } 611 | -------------------------------------------------------------------------------- /NetworkSample/NetworkSampleTests/JSON/character_list_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "count": 826, 4 | "pages": 42, 5 | "next": "https://rickandmortyapi.com/api/character?page=2", 6 | "prev": null 7 | }, 8 | "results": [ 9 | { 10 | "id": 1, 11 | "name": "Rick Sanchez", 12 | "status": "Alive", 13 | "species": "Human", 14 | "type": "", 15 | "gender": "Male", 16 | "origin": { 17 | "name": "Earth (C-137)", 18 | "url": "https://rickandmortyapi.com/api/location/1" 19 | }, 20 | "location": { 21 | "name": "Citadel of Ricks", 22 | "url": "https://rickandmortyapi.com/api/location/3" 23 | }, 24 | "image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg", 25 | "episode": [ 26 | "https://rickandmortyapi.com/api/episode/1", 27 | "https://rickandmortyapi.com/api/episode/2", 28 | "https://rickandmortyapi.com/api/episode/3", 29 | "https://rickandmortyapi.com/api/episode/4", 30 | "https://rickandmortyapi.com/api/episode/5", 31 | "https://rickandmortyapi.com/api/episode/6", 32 | "https://rickandmortyapi.com/api/episode/7", 33 | "https://rickandmortyapi.com/api/episode/8", 34 | "https://rickandmortyapi.com/api/episode/9", 35 | "https://rickandmortyapi.com/api/episode/10", 36 | "https://rickandmortyapi.com/api/episode/11", 37 | "https://rickandmortyapi.com/api/episode/12", 38 | "https://rickandmortyapi.com/api/episode/13", 39 | "https://rickandmortyapi.com/api/episode/14", 40 | "https://rickandmortyapi.com/api/episode/15", 41 | "https://rickandmortyapi.com/api/episode/16", 42 | "https://rickandmortyapi.com/api/episode/17", 43 | "https://rickandmortyapi.com/api/episode/18", 44 | "https://rickandmortyapi.com/api/episode/19", 45 | "https://rickandmortyapi.com/api/episode/20", 46 | "https://rickandmortyapi.com/api/episode/21", 47 | "https://rickandmortyapi.com/api/episode/22", 48 | "https://rickandmortyapi.com/api/episode/23", 49 | "https://rickandmortyapi.com/api/episode/24", 50 | "https://rickandmortyapi.com/api/episode/25", 51 | "https://rickandmortyapi.com/api/episode/26", 52 | "https://rickandmortyapi.com/api/episode/27", 53 | "https://rickandmortyapi.com/api/episode/28", 54 | "https://rickandmortyapi.com/api/episode/29", 55 | "https://rickandmortyapi.com/api/episode/30", 56 | "https://rickandmortyapi.com/api/episode/31", 57 | "https://rickandmortyapi.com/api/episode/32", 58 | "https://rickandmortyapi.com/api/episode/33", 59 | "https://rickandmortyapi.com/api/episode/34", 60 | "https://rickandmortyapi.com/api/episode/35", 61 | "https://rickandmortyapi.com/api/episode/36", 62 | "https://rickandmortyapi.com/api/episode/37", 63 | "https://rickandmortyapi.com/api/episode/38", 64 | "https://rickandmortyapi.com/api/episode/39", 65 | "https://rickandmortyapi.com/api/episode/40", 66 | "https://rickandmortyapi.com/api/episode/41", 67 | "https://rickandmortyapi.com/api/episode/42", 68 | "https://rickandmortyapi.com/api/episode/43", 69 | "https://rickandmortyapi.com/api/episode/44", 70 | "https://rickandmortyapi.com/api/episode/45", 71 | "https://rickandmortyapi.com/api/episode/46", 72 | "https://rickandmortyapi.com/api/episode/47", 73 | "https://rickandmortyapi.com/api/episode/48", 74 | "https://rickandmortyapi.com/api/episode/49", 75 | "https://rickandmortyapi.com/api/episode/50", 76 | "https://rickandmortyapi.com/api/episode/51" 77 | ], 78 | "url": "https://rickandmortyapi.com/api/character/1", 79 | "created": "2017-11-04T18:48:46.250Z" 80 | }, 81 | { 82 | "id": 2, 83 | "name": "Morty Smith", 84 | "status": "Alive", 85 | "species": "Human", 86 | "type": "", 87 | "gender": "Male", 88 | "origin": { 89 | "name": "unknown", 90 | "url": "" 91 | }, 92 | "location": { 93 | "name": "Citadel of Ricks", 94 | "url": "https://rickandmortyapi.com/api/location/3" 95 | }, 96 | "image": "https://rickandmortyapi.com/api/character/avatar/2.jpeg", 97 | "episode": [ 98 | "https://rickandmortyapi.com/api/episode/1", 99 | "https://rickandmortyapi.com/api/episode/2", 100 | "https://rickandmortyapi.com/api/episode/3", 101 | "https://rickandmortyapi.com/api/episode/4", 102 | "https://rickandmortyapi.com/api/episode/5", 103 | "https://rickandmortyapi.com/api/episode/6", 104 | "https://rickandmortyapi.com/api/episode/7", 105 | "https://rickandmortyapi.com/api/episode/8", 106 | "https://rickandmortyapi.com/api/episode/9", 107 | "https://rickandmortyapi.com/api/episode/10", 108 | "https://rickandmortyapi.com/api/episode/11", 109 | "https://rickandmortyapi.com/api/episode/12", 110 | "https://rickandmortyapi.com/api/episode/13", 111 | "https://rickandmortyapi.com/api/episode/14", 112 | "https://rickandmortyapi.com/api/episode/15", 113 | "https://rickandmortyapi.com/api/episode/16", 114 | "https://rickandmortyapi.com/api/episode/17", 115 | "https://rickandmortyapi.com/api/episode/18", 116 | "https://rickandmortyapi.com/api/episode/19", 117 | "https://rickandmortyapi.com/api/episode/20", 118 | "https://rickandmortyapi.com/api/episode/21", 119 | "https://rickandmortyapi.com/api/episode/22", 120 | "https://rickandmortyapi.com/api/episode/23", 121 | "https://rickandmortyapi.com/api/episode/24", 122 | "https://rickandmortyapi.com/api/episode/25", 123 | "https://rickandmortyapi.com/api/episode/26", 124 | "https://rickandmortyapi.com/api/episode/27", 125 | "https://rickandmortyapi.com/api/episode/28", 126 | "https://rickandmortyapi.com/api/episode/29", 127 | "https://rickandmortyapi.com/api/episode/30", 128 | "https://rickandmortyapi.com/api/episode/31", 129 | "https://rickandmortyapi.com/api/episode/32", 130 | "https://rickandmortyapi.com/api/episode/33", 131 | "https://rickandmortyapi.com/api/episode/34", 132 | "https://rickandmortyapi.com/api/episode/35", 133 | "https://rickandmortyapi.com/api/episode/36", 134 | "https://rickandmortyapi.com/api/episode/37", 135 | "https://rickandmortyapi.com/api/episode/38", 136 | "https://rickandmortyapi.com/api/episode/39", 137 | "https://rickandmortyapi.com/api/episode/40", 138 | "https://rickandmortyapi.com/api/episode/41", 139 | "https://rickandmortyapi.com/api/episode/42", 140 | "https://rickandmortyapi.com/api/episode/43", 141 | "https://rickandmortyapi.com/api/episode/44", 142 | "https://rickandmortyapi.com/api/episode/45", 143 | "https://rickandmortyapi.com/api/episode/46", 144 | "https://rickandmortyapi.com/api/episode/47", 145 | "https://rickandmortyapi.com/api/episode/48", 146 | "https://rickandmortyapi.com/api/episode/49", 147 | "https://rickandmortyapi.com/api/episode/50", 148 | "https://rickandmortyapi.com/api/episode/51" 149 | ], 150 | "url": "https://rickandmortyapi.com/api/character/2", 151 | "created": "2017-11-04T18:50:21.651Z" 152 | }, 153 | { 154 | "id": 3, 155 | "name": "Summer Smith", 156 | "status": "Alive", 157 | "species": "Human", 158 | "type": "", 159 | "gender": "Female", 160 | "origin": { 161 | "name": "Earth (Replacement Dimension)", 162 | "url": "https://rickandmortyapi.com/api/location/20" 163 | }, 164 | "location": { 165 | "name": "Earth (Replacement Dimension)", 166 | "url": "https://rickandmortyapi.com/api/location/20" 167 | }, 168 | "image": "https://rickandmortyapi.com/api/character/avatar/3.jpeg", 169 | "episode": [ 170 | "https://rickandmortyapi.com/api/episode/6", 171 | "https://rickandmortyapi.com/api/episode/7", 172 | "https://rickandmortyapi.com/api/episode/8", 173 | "https://rickandmortyapi.com/api/episode/9", 174 | "https://rickandmortyapi.com/api/episode/10", 175 | "https://rickandmortyapi.com/api/episode/11", 176 | "https://rickandmortyapi.com/api/episode/12", 177 | "https://rickandmortyapi.com/api/episode/14", 178 | "https://rickandmortyapi.com/api/episode/15", 179 | "https://rickandmortyapi.com/api/episode/16", 180 | "https://rickandmortyapi.com/api/episode/17", 181 | "https://rickandmortyapi.com/api/episode/18", 182 | "https://rickandmortyapi.com/api/episode/19", 183 | "https://rickandmortyapi.com/api/episode/20", 184 | "https://rickandmortyapi.com/api/episode/21", 185 | "https://rickandmortyapi.com/api/episode/22", 186 | "https://rickandmortyapi.com/api/episode/23", 187 | "https://rickandmortyapi.com/api/episode/24", 188 | "https://rickandmortyapi.com/api/episode/25", 189 | "https://rickandmortyapi.com/api/episode/26", 190 | "https://rickandmortyapi.com/api/episode/27", 191 | "https://rickandmortyapi.com/api/episode/29", 192 | "https://rickandmortyapi.com/api/episode/30", 193 | "https://rickandmortyapi.com/api/episode/31", 194 | "https://rickandmortyapi.com/api/episode/32", 195 | "https://rickandmortyapi.com/api/episode/33", 196 | "https://rickandmortyapi.com/api/episode/34", 197 | "https://rickandmortyapi.com/api/episode/35", 198 | "https://rickandmortyapi.com/api/episode/36", 199 | "https://rickandmortyapi.com/api/episode/38", 200 | "https://rickandmortyapi.com/api/episode/39", 201 | "https://rickandmortyapi.com/api/episode/40", 202 | "https://rickandmortyapi.com/api/episode/41", 203 | "https://rickandmortyapi.com/api/episode/42", 204 | "https://rickandmortyapi.com/api/episode/43", 205 | "https://rickandmortyapi.com/api/episode/44", 206 | "https://rickandmortyapi.com/api/episode/45", 207 | "https://rickandmortyapi.com/api/episode/46", 208 | "https://rickandmortyapi.com/api/episode/47", 209 | "https://rickandmortyapi.com/api/episode/48", 210 | "https://rickandmortyapi.com/api/episode/49", 211 | "https://rickandmortyapi.com/api/episode/51" 212 | ], 213 | "url": "https://rickandmortyapi.com/api/character/3", 214 | "created": "2017-11-04T19:09:56.428Z" 215 | }, 216 | { 217 | "id": 4, 218 | "name": "Beth Smith", 219 | "status": "Alive", 220 | "species": "Human", 221 | "type": "", 222 | "gender": "Female", 223 | "origin": { 224 | "name": "Earth (Replacement Dimension)", 225 | "url": "https://rickandmortyapi.com/api/location/20" 226 | }, 227 | "location": { 228 | "name": "Earth (Replacement Dimension)", 229 | "url": "https://rickandmortyapi.com/api/location/20" 230 | }, 231 | "image": "https://rickandmortyapi.com/api/character/avatar/4.jpeg", 232 | "episode": [ 233 | "https://rickandmortyapi.com/api/episode/6", 234 | "https://rickandmortyapi.com/api/episode/7", 235 | "https://rickandmortyapi.com/api/episode/8", 236 | "https://rickandmortyapi.com/api/episode/9", 237 | "https://rickandmortyapi.com/api/episode/10", 238 | "https://rickandmortyapi.com/api/episode/11", 239 | "https://rickandmortyapi.com/api/episode/12", 240 | "https://rickandmortyapi.com/api/episode/14", 241 | "https://rickandmortyapi.com/api/episode/15", 242 | "https://rickandmortyapi.com/api/episode/16", 243 | "https://rickandmortyapi.com/api/episode/18", 244 | "https://rickandmortyapi.com/api/episode/19", 245 | "https://rickandmortyapi.com/api/episode/20", 246 | "https://rickandmortyapi.com/api/episode/21", 247 | "https://rickandmortyapi.com/api/episode/22", 248 | "https://rickandmortyapi.com/api/episode/23", 249 | "https://rickandmortyapi.com/api/episode/24", 250 | "https://rickandmortyapi.com/api/episode/25", 251 | "https://rickandmortyapi.com/api/episode/26", 252 | "https://rickandmortyapi.com/api/episode/27", 253 | "https://rickandmortyapi.com/api/episode/28", 254 | "https://rickandmortyapi.com/api/episode/29", 255 | "https://rickandmortyapi.com/api/episode/30", 256 | "https://rickandmortyapi.com/api/episode/31", 257 | "https://rickandmortyapi.com/api/episode/32", 258 | "https://rickandmortyapi.com/api/episode/33", 259 | "https://rickandmortyapi.com/api/episode/34", 260 | "https://rickandmortyapi.com/api/episode/35", 261 | "https://rickandmortyapi.com/api/episode/36", 262 | "https://rickandmortyapi.com/api/episode/38", 263 | "https://rickandmortyapi.com/api/episode/39", 264 | "https://rickandmortyapi.com/api/episode/40", 265 | "https://rickandmortyapi.com/api/episode/41", 266 | "https://rickandmortyapi.com/api/episode/42", 267 | "https://rickandmortyapi.com/api/episode/43", 268 | "https://rickandmortyapi.com/api/episode/44", 269 | "https://rickandmortyapi.com/api/episode/45", 270 | "https://rickandmortyapi.com/api/episode/46", 271 | "https://rickandmortyapi.com/api/episode/47", 272 | "https://rickandmortyapi.com/api/episode/48", 273 | "https://rickandmortyapi.com/api/episode/49", 274 | "https://rickandmortyapi.com/api/episode/51" 275 | ], 276 | "url": "https://rickandmortyapi.com/api/character/4", 277 | "created": "2017-11-04T19:22:43.665Z" 278 | }, 279 | { 280 | "id": 5, 281 | "name": "Jerry Smith", 282 | "status": "Alive", 283 | "species": "Human", 284 | "type": "", 285 | "gender": "Male", 286 | "origin": { 287 | "name": "Earth (Replacement Dimension)", 288 | "url": "https://rickandmortyapi.com/api/location/20" 289 | }, 290 | "location": { 291 | "name": "Earth (Replacement Dimension)", 292 | "url": "https://rickandmortyapi.com/api/location/20" 293 | }, 294 | "image": "https://rickandmortyapi.com/api/character/avatar/5.jpeg", 295 | "episode": [ 296 | "https://rickandmortyapi.com/api/episode/6", 297 | "https://rickandmortyapi.com/api/episode/7", 298 | "https://rickandmortyapi.com/api/episode/8", 299 | "https://rickandmortyapi.com/api/episode/9", 300 | "https://rickandmortyapi.com/api/episode/10", 301 | "https://rickandmortyapi.com/api/episode/11", 302 | "https://rickandmortyapi.com/api/episode/12", 303 | "https://rickandmortyapi.com/api/episode/13", 304 | "https://rickandmortyapi.com/api/episode/14", 305 | "https://rickandmortyapi.com/api/episode/15", 306 | "https://rickandmortyapi.com/api/episode/16", 307 | "https://rickandmortyapi.com/api/episode/18", 308 | "https://rickandmortyapi.com/api/episode/19", 309 | "https://rickandmortyapi.com/api/episode/20", 310 | "https://rickandmortyapi.com/api/episode/21", 311 | "https://rickandmortyapi.com/api/episode/22", 312 | "https://rickandmortyapi.com/api/episode/23", 313 | "https://rickandmortyapi.com/api/episode/26", 314 | "https://rickandmortyapi.com/api/episode/29", 315 | "https://rickandmortyapi.com/api/episode/30", 316 | "https://rickandmortyapi.com/api/episode/31", 317 | "https://rickandmortyapi.com/api/episode/32", 318 | "https://rickandmortyapi.com/api/episode/33", 319 | "https://rickandmortyapi.com/api/episode/35", 320 | "https://rickandmortyapi.com/api/episode/36", 321 | "https://rickandmortyapi.com/api/episode/38", 322 | "https://rickandmortyapi.com/api/episode/39", 323 | "https://rickandmortyapi.com/api/episode/40", 324 | "https://rickandmortyapi.com/api/episode/41", 325 | "https://rickandmortyapi.com/api/episode/42", 326 | "https://rickandmortyapi.com/api/episode/43", 327 | "https://rickandmortyapi.com/api/episode/44", 328 | "https://rickandmortyapi.com/api/episode/45", 329 | "https://rickandmortyapi.com/api/episode/46", 330 | "https://rickandmortyapi.com/api/episode/47", 331 | "https://rickandmortyapi.com/api/episode/48", 332 | "https://rickandmortyapi.com/api/episode/49", 333 | "https://rickandmortyapi.com/api/episode/50", 334 | "https://rickandmortyapi.com/api/episode/51" 335 | ], 336 | "url": "https://rickandmortyapi.com/api/character/5", 337 | "created": "2017-11-04T19:26:56.301Z" 338 | }, 339 | { 340 | "id": 6, 341 | "name": "Abadango Cluster Princess", 342 | "status": "Alive", 343 | "species": "Alien", 344 | "type": "", 345 | "gender": "Female", 346 | "origin": { 347 | "name": "Abadango", 348 | "url": "https://rickandmortyapi.com/api/location/2" 349 | }, 350 | "location": { 351 | "name": "Abadango", 352 | "url": "https://rickandmortyapi.com/api/location/2" 353 | }, 354 | "image": "https://rickandmortyapi.com/api/character/avatar/6.jpeg", 355 | "episode": [ 356 | "https://rickandmortyapi.com/api/episode/27" 357 | ], 358 | "url": "https://rickandmortyapi.com/api/character/6", 359 | "created": "2017-11-04T19:50:28.250Z" 360 | }, 361 | { 362 | "id": 7, 363 | "name": "Abradolf Lincler", 364 | "status": "unknown", 365 | "species": "Human", 366 | "type": "Genetic experiment", 367 | "gender": "Male", 368 | "origin": { 369 | "name": "Earth (Replacement Dimension)", 370 | "url": "https://rickandmortyapi.com/api/location/20" 371 | }, 372 | "location": { 373 | "name": "Testicle Monster Dimension", 374 | "url": "https://rickandmortyapi.com/api/location/21" 375 | }, 376 | "image": "https://rickandmortyapi.com/api/character/avatar/7.jpeg", 377 | "episode": [ 378 | "https://rickandmortyapi.com/api/episode/10", 379 | "https://rickandmortyapi.com/api/episode/11" 380 | ], 381 | "url": "https://rickandmortyapi.com/api/character/7", 382 | "created": "2017-11-04T19:59:20.523Z" 383 | }, 384 | { 385 | "id": 8, 386 | "name": "Adjudicator Rick", 387 | "status": "Dead", 388 | "species": "Human", 389 | "type": "", 390 | "gender": "Male", 391 | "origin": { 392 | "name": "unknown", 393 | "url": "" 394 | }, 395 | "location": { 396 | "name": "Citadel of Ricks", 397 | "url": "https://rickandmortyapi.com/api/location/3" 398 | }, 399 | "image": "https://rickandmortyapi.com/api/character/avatar/8.jpeg", 400 | "episode": [ 401 | "https://rickandmortyapi.com/api/episode/28" 402 | ], 403 | "url": "https://rickandmortyapi.com/api/character/8", 404 | "created": "2017-11-04T20:03:34.737Z" 405 | }, 406 | { 407 | "id": 9, 408 | "name": "Agency Director", 409 | "status": "Dead", 410 | "species": "Human", 411 | "type": "", 412 | "gender": "Male", 413 | "origin": { 414 | "name": "Earth (Replacement Dimension)", 415 | "url": "https://rickandmortyapi.com/api/location/20" 416 | }, 417 | "location": { 418 | "name": "Earth (Replacement Dimension)", 419 | "url": "https://rickandmortyapi.com/api/location/20" 420 | }, 421 | "image": "https://rickandmortyapi.com/api/character/avatar/9.jpeg", 422 | "episode": [ 423 | "https://rickandmortyapi.com/api/episode/24" 424 | ], 425 | "url": "https://rickandmortyapi.com/api/character/9", 426 | "created": "2017-11-04T20:06:54.976Z" 427 | }, 428 | { 429 | "id": 10, 430 | "name": "Alan Rails", 431 | "status": "Dead", 432 | "species": "Human", 433 | "type": "Superhuman (Ghost trains summoner)", 434 | "gender": "Male", 435 | "origin": { 436 | "name": "unknown", 437 | "url": "" 438 | }, 439 | "location": { 440 | "name": "Worldender's lair", 441 | "url": "https://rickandmortyapi.com/api/location/4" 442 | }, 443 | "image": "https://rickandmortyapi.com/api/character/avatar/10.jpeg", 444 | "episode": [ 445 | "https://rickandmortyapi.com/api/episode/25" 446 | ], 447 | "url": "https://rickandmortyapi.com/api/character/10", 448 | "created": "2017-11-04T20:19:09.017Z" 449 | }, 450 | { 451 | "id": 11, 452 | "name": "Albert Einstein", 453 | "status": "Dead", 454 | "species": "Human", 455 | "type": "", 456 | "gender": "Male", 457 | "origin": { 458 | "name": "Earth (C-137)", 459 | "url": "https://rickandmortyapi.com/api/location/1" 460 | }, 461 | "location": { 462 | "name": "Earth (Replacement Dimension)", 463 | "url": "https://rickandmortyapi.com/api/location/20" 464 | }, 465 | "image": "https://rickandmortyapi.com/api/character/avatar/11.jpeg", 466 | "episode": [ 467 | "https://rickandmortyapi.com/api/episode/12" 468 | ], 469 | "url": "https://rickandmortyapi.com/api/character/11", 470 | "created": "2017-11-04T20:20:20.965Z" 471 | }, 472 | { 473 | "id": 12, 474 | "name": "Alexander", 475 | "status": "Dead", 476 | "species": "Human", 477 | "type": "", 478 | "gender": "Male", 479 | "origin": { 480 | "name": "Earth (C-137)", 481 | "url": "https://rickandmortyapi.com/api/location/1" 482 | }, 483 | "location": { 484 | "name": "Anatomy Park", 485 | "url": "https://rickandmortyapi.com/api/location/5" 486 | }, 487 | "image": "https://rickandmortyapi.com/api/character/avatar/12.jpeg", 488 | "episode": [ 489 | "https://rickandmortyapi.com/api/episode/3" 490 | ], 491 | "url": "https://rickandmortyapi.com/api/character/12", 492 | "created": "2017-11-04T20:32:33.144Z" 493 | }, 494 | { 495 | "id": 13, 496 | "name": "Alien Googah", 497 | "status": "unknown", 498 | "species": "Alien", 499 | "type": "", 500 | "gender": "unknown", 501 | "origin": { 502 | "name": "unknown", 503 | "url": "" 504 | }, 505 | "location": { 506 | "name": "Earth (Replacement Dimension)", 507 | "url": "https://rickandmortyapi.com/api/location/20" 508 | }, 509 | "image": "https://rickandmortyapi.com/api/character/avatar/13.jpeg", 510 | "episode": [ 511 | "https://rickandmortyapi.com/api/episode/31" 512 | ], 513 | "url": "https://rickandmortyapi.com/api/character/13", 514 | "created": "2017-11-04T20:33:30.779Z" 515 | }, 516 | { 517 | "id": 14, 518 | "name": "Alien Morty", 519 | "status": "unknown", 520 | "species": "Alien", 521 | "type": "", 522 | "gender": "Male", 523 | "origin": { 524 | "name": "unknown", 525 | "url": "" 526 | }, 527 | "location": { 528 | "name": "Citadel of Ricks", 529 | "url": "https://rickandmortyapi.com/api/location/3" 530 | }, 531 | "image": "https://rickandmortyapi.com/api/character/avatar/14.jpeg", 532 | "episode": [ 533 | "https://rickandmortyapi.com/api/episode/10" 534 | ], 535 | "url": "https://rickandmortyapi.com/api/character/14", 536 | "created": "2017-11-04T20:51:31.373Z" 537 | }, 538 | { 539 | "id": 15, 540 | "name": "Alien Rick", 541 | "status": "unknown", 542 | "species": "Alien", 543 | "type": "", 544 | "gender": "Male", 545 | "origin": { 546 | "name": "unknown", 547 | "url": "" 548 | }, 549 | "location": { 550 | "name": "Citadel of Ricks", 551 | "url": "https://rickandmortyapi.com/api/location/3" 552 | }, 553 | "image": "https://rickandmortyapi.com/api/character/avatar/15.jpeg", 554 | "episode": [ 555 | "https://rickandmortyapi.com/api/episode/10" 556 | ], 557 | "url": "https://rickandmortyapi.com/api/character/15", 558 | "created": "2017-11-04T20:56:13.215Z" 559 | }, 560 | { 561 | "id": 16, 562 | "name": "Amish Cyborg", 563 | "status": "Dead", 564 | "species": "Alien", 565 | "type": "Parasite", 566 | "gender": "Male", 567 | "origin": { 568 | "name": "unknown", 569 | "url": "" 570 | }, 571 | "location": { 572 | "name": "Earth (Replacement Dimension)", 573 | "url": "https://rickandmortyapi.com/api/location/20" 574 | }, 575 | "image": "https://rickandmortyapi.com/api/character/avatar/16.jpeg", 576 | "episode": [ 577 | "https://rickandmortyapi.com/api/episode/15" 578 | ], 579 | "url": "https://rickandmortyapi.com/api/character/16", 580 | "created": "2017-11-04T21:12:45.235Z" 581 | }, 582 | { 583 | "id": 17, 584 | "name": "Annie", 585 | "status": "Alive", 586 | "species": "Human", 587 | "type": "", 588 | "gender": "Female", 589 | "origin": { 590 | "name": "Earth (C-137)", 591 | "url": "https://rickandmortyapi.com/api/location/1" 592 | }, 593 | "location": { 594 | "name": "Anatomy Park", 595 | "url": "https://rickandmortyapi.com/api/location/5" 596 | }, 597 | "image": "https://rickandmortyapi.com/api/character/avatar/17.jpeg", 598 | "episode": [ 599 | "https://rickandmortyapi.com/api/episode/3" 600 | ], 601 | "url": "https://rickandmortyapi.com/api/character/17", 602 | "created": "2017-11-04T22:21:24.481Z" 603 | }, 604 | { 605 | "id": 18, 606 | "name": "Antenna Morty", 607 | "status": "Alive", 608 | "species": "Human", 609 | "type": "Human with antennae", 610 | "gender": "Male", 611 | "origin": { 612 | "name": "unknown", 613 | "url": "" 614 | }, 615 | "location": { 616 | "name": "Citadel of Ricks", 617 | "url": "https://rickandmortyapi.com/api/location/3" 618 | }, 619 | "image": "https://rickandmortyapi.com/api/character/avatar/18.jpeg", 620 | "episode": [ 621 | "https://rickandmortyapi.com/api/episode/10", 622 | "https://rickandmortyapi.com/api/episode/28" 623 | ], 624 | "url": "https://rickandmortyapi.com/api/character/18", 625 | "created": "2017-11-04T22:25:29.008Z" 626 | }, 627 | { 628 | "id": 19, 629 | "name": "Antenna Rick", 630 | "status": "unknown", 631 | "species": "Human", 632 | "type": "Human with antennae", 633 | "gender": "Male", 634 | "origin": { 635 | "name": "unknown", 636 | "url": "" 637 | }, 638 | "location": { 639 | "name": "unknown", 640 | "url": "" 641 | }, 642 | "image": "https://rickandmortyapi.com/api/character/avatar/19.jpeg", 643 | "episode": [ 644 | "https://rickandmortyapi.com/api/episode/10" 645 | ], 646 | "url": "https://rickandmortyapi.com/api/character/19", 647 | "created": "2017-11-04T22:28:13.756Z" 648 | }, 649 | { 650 | "id": 20, 651 | "name": "Ants in my Eyes Johnson", 652 | "status": "unknown", 653 | "species": "Human", 654 | "type": "Human with ants in his eyes", 655 | "gender": "Male", 656 | "origin": { 657 | "name": "unknown", 658 | "url": "" 659 | }, 660 | "location": { 661 | "name": "Interdimensional Cable", 662 | "url": "https://rickandmortyapi.com/api/location/6" 663 | }, 664 | "image": "https://rickandmortyapi.com/api/character/avatar/20.jpeg", 665 | "episode": [ 666 | "https://rickandmortyapi.com/api/episode/8" 667 | ], 668 | "url": "https://rickandmortyapi.com/api/character/20", 669 | "created": "2017-11-04T22:34:53.659Z" 670 | } 671 | ] 672 | } 673 | --------------------------------------------------------------------------------