├── 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 |
--------------------------------------------------------------------------------