Skip to content

Commit

Permalink
[DEV][Developed network layer]
Browse files Browse the repository at this point in the history
  • Loading branch information
developersancho committed Feb 26, 2022
1 parent e0e6311 commit 9ebeda8
Show file tree
Hide file tree
Showing 26 changed files with 373 additions and 80 deletions.
21 changes: 21 additions & 0 deletions Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SwiftRorty.iOS' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!

# Pods for SwiftRorty.iOS
pod 'lottie-ios'
pod "Resolver"

target 'SwiftRorty.iOSTests' do
inherit! :search_paths
# Pods for testing
end

target 'SwiftRorty.iOSUITests' do
# Pods for testing
end

end
26 changes: 23 additions & 3 deletions SwiftRorty.iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
E2991AB927AC76FA00BF4B9A /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2991AB827AC76FA00BF4B9A /* Location.swift */; };
E2991ABB27AC771E00BF4B9A /* CharacterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2991ABA27AC771E00BF4B9A /* CharacterInfo.swift */; };
E2991ABD27AC777300BF4B9A /* CharacterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2991ABC27AC777300BF4B9A /* CharacterResponse.swift */; };
E2AD779A27CAC1E8006565C2 /* RestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AD779927CAC1E8006565C2 /* RestClient.swift */; };
E2AD779C27CAC242006565C2 /* RestClientErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AD779B27CAC242006565C2 /* RestClientErrors.swift */; };
E2AD779E27CACD8D006565C2 /* CharacterListDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AD779D27CACD8D006565C2 /* CharacterListDto.swift */; };
E2D5A98327C96E3E0017DA32 /* APIMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D5A98227C96E3E0017DA32 /* APIMethod.swift */; };
E2D5A98527C974010017DA32 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D5A98427C974010017DA32 /* APIClient.swift */; };
E2D5A98727C9771D0017DA32 /* APIClientImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D5A98627C9771D0017DA32 /* APIClientImpl.swift */; };
Expand Down Expand Up @@ -119,6 +122,9 @@
E2991AB827AC76FA00BF4B9A /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; };
E2991ABA27AC771E00BF4B9A /* CharacterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterInfo.swift; sourceTree = "<group>"; };
E2991ABC27AC777300BF4B9A /* CharacterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterResponse.swift; sourceTree = "<group>"; };
E2AD779927CAC1E8006565C2 /* RestClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClient.swift; sourceTree = "<group>"; };
E2AD779B27CAC242006565C2 /* RestClientErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClientErrors.swift; sourceTree = "<group>"; };
E2AD779D27CACD8D006565C2 /* CharacterListDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListDto.swift; sourceTree = "<group>"; };
E2D5A98227C96E3E0017DA32 /* APIMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMethod.swift; sourceTree = "<group>"; };
E2D5A98427C974010017DA32 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
E2D5A98627C9771D0017DA32 /* APIClientImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientImpl.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -296,7 +302,8 @@
E2291D6327A5E992006D3FD8 /* core */ = {
isa = PBXGroup;
children = (
E28A38C027C94B19005DD497 /* network */,
E2AD779827CAC1CE006565C2 /* network */,
E28A38C027C94B19005DD497 /* network_2 */,
);
path = core;
sourceTree = "<group>";
Expand Down Expand Up @@ -382,7 +389,7 @@
path = component;
sourceTree = "<group>";
};
E28A38C027C94B19005DD497 /* network */ = {
E28A38C027C94B19005DD497 /* network_2 */ = {
isa = PBXGroup;
children = (
E2D5A98227C96E3E0017DA32 /* APIMethod.swift */,
Expand All @@ -392,7 +399,7 @@
E2D5A98A27C977EF0017DA32 /* Decoder.swift */,
E2D5A98E27C97CEC0017DA32 /* URLRequest.swift */,
);
path = network;
path = network_2;
sourceTree = "<group>";
};
E2991AA527AC729500BF4B9A /* remote */ = {
Expand Down Expand Up @@ -421,6 +428,7 @@
E2512DAF27C82B0000FC6129 /* KeyValueModel.swift */,
E2512DB127C830FF00FC6129 /* StringExtension.swift */,
E28A38BE27C94730005DD497 /* DtoExtension.swift */,
E2AD779D27CACD8D006565C2 /* CharacterListDto.swift */,
);
path = dto;
sourceTree = "<group>";
Expand Down Expand Up @@ -454,6 +462,15 @@
path = location;
sourceTree = "<group>";
};
E2AD779827CAC1CE006565C2 /* network */ = {
isa = PBXGroup;
children = (
E2AD779927CAC1E8006565C2 /* RestClient.swift */,
E2AD779B27CAC242006565C2 /* RestClientErrors.swift */,
);
path = network;
sourceTree = "<group>";
};
FE5D0614452C9D5795CE3E9B /* Pods */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -712,6 +729,7 @@
E2555C1327A872FF007E5266 /* SplashScreen.swift in Sources */,
E2991AB327AC766300BF4B9A /* EpisodeInfo.swift in Sources */,
E2512DB027C82B0000FC6129 /* KeyValueModel.swift in Sources */,
E2AD779A27CAC1E8006565C2 /* RestClient.swift in Sources */,
E2D5A98327C96E3E0017DA32 /* APIMethod.swift in Sources */,
E2D5A99127C9986C0017DA32 /* CharacterRow.swift in Sources */,
E2991AAF27AC740800BF4B9A /* LocationInfo.swift in Sources */,
Expand All @@ -731,6 +749,7 @@
E2555BFF27A87181007E5266 /* PageInfo.swift in Sources */,
E2991ABD27AC777300BF4B9A /* CharacterResponse.swift in Sources */,
E2512DAC27C82A5E00FC6129 /* LocationDto.swift in Sources */,
E2AD779C27CAC242006565C2 /* RestClientErrors.swift in Sources */,
E2555C0327A87220007E5266 /* FavoriteDao.swift in Sources */,
E2555C0F27A872E4007E5266 /* DetailScreen.swift in Sources */,
E2D5A98727C9771D0017DA32 /* APIClientImpl.swift in Sources */,
Expand All @@ -743,6 +762,7 @@
E28A38BF27C94730005DD497 /* DtoExtension.swift in Sources */,
E2291D3327A5E908006D3FD8 /* SwiftRorty_iOSApp.swift in Sources */,
E2991AB727AC76D400BF4B9A /* Origin.swift in Sources */,
E2AD779E27CACD8D006565C2 /* CharacterListDto.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<key>SwiftRorty.iOS.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
<integer>5</integer>
</dict>
</dict>
</dict>
Expand Down
118 changes: 118 additions & 0 deletions SwiftRorty.iOS/core/network/RestClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// RestClient.swift
// SwiftRorty.iOS
//
// Created by developersancho on 26.02.2022.
//

import Foundation

import Combine

/// Provides access to the REST Backend
protocol RestClient {
/// Retrieves a JSON resource and decodes it
func get<T: Decodable, E: Endpoint>(_ endpoint: E) -> AnyPublisher<T, Error>

/// Creates some resource by sending a JSON body and returning empty response
func post<S: Encodable, E: Endpoint>(_ endpoint: E, using body: S)
-> AnyPublisher<Void, Error>
}

class RestClientImpl: RestClient {
private let session: URLSession

init(sessionConfig: URLSessionConfiguration? = nil) {
self.session = URLSession(configuration: sessionConfig ?? URLSessionConfiguration.default)
}

func get<T, E>(_ endpoint: E) -> AnyPublisher<T, Error> where T: Decodable, E: Endpoint {
startRequest(for: endpoint, method: "GET", jsonBody: nil as String?)
.tryMap { try $0.parseJson() }
.eraseToAnyPublisher()
}

func post<S, E>(_ endpoint: E, using body: S)
-> AnyPublisher<Void, Error> where S: Encodable, E: Endpoint
{
startRequest(for: endpoint, method: "POST", jsonBody: body)
.map { _ in () }
.catch { error -> AnyPublisher<Void, Error> in
switch error {
case RestClientErrors.noDataReceived:
// API's Post request doesn't return data back even with code 200
return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
default:
return Fail<Void, Error>(error: error).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}

private func startRequest<T: Encodable, S: Endpoint>(for endpoint: S,
method: String,
jsonBody: T? = nil)
-> AnyPublisher<InterimRestResponse, Error> {
var request: URLRequest

do {
request = try buildRequest(endpoint: endpoint, method: method, jsonBody: jsonBody)
} catch {
print("Failed to create request: \(String(describing: error))")
return Fail(error: error).eraseToAnyPublisher()
}

print("Starting \(method) request for \(String(describing: request))")

return session.dataTaskPublisher(for: request)
.mapError { (error: Error) -> Error in
print("Request failed: \(String(describing: error))")
return RestClientErrors.requestFailed(error: error)
}
// we got a response, lets see what kind of response
.tryMap { (data: Data, response: URLResponse) in
let response = response as! HTTPURLResponse
print("Got response with status code \(response.statusCode) and \(data.count) bytes of data")

if response.statusCode == 400 {
throw RestClientErrors.requestFailed(code: response.statusCode)
}
return InterimRestResponse(data: data, response: response)
}.eraseToAnyPublisher()
}

private func buildRequest<T: Encodable, S: Endpoint>(endpoint: S,
method: String,
jsonBody: T?) throws -> URLRequest {
var request = URLRequest(url: endpoint.url, timeoutInterval: 10)
request.httpMethod = method
// if we got some data, we encode as JSON and put it in the request
if let body = jsonBody {
do {
request.httpBody = try JSONEncoder().encode(body)
} catch {
throw RestClientErrors.jsonDecode(error: error)
}
}
return request
}

struct InterimRestResponse {
let data: Data
let response: HTTPURLResponse

func parseJson<T: Decodable>() throws -> T {
if data.isEmpty {
throw RestClientErrors.noDataReceived
}

do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
print("Failed to decode JSON: \(error)", String(describing: error))
throw RestClientErrors.jsonDecode(error: error)
}
}
}

}
15 changes: 15 additions & 0 deletions SwiftRorty.iOS/core/network/RestClientErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// RestClientErrors.swift
// SwiftRorty.iOS
//
// Created by developersancho on 26.02.2022.
//

import Foundation

enum RestClientErrors: Error {
case requestFailed(error: Error)
case requestFailed(code: Int)
case noDataReceived
case jsonDecode(error: Error)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions SwiftRorty.iOS/core/network_2/Decoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Decoder.swift
// SwiftRorty.iOS
//
// Created by developersancho on 25.02.2022.
//

import Foundation
import Combine

class Decoder {
func decode<T: Decodable>(_ data: Data) -> AnyPublisher<T, BaseError> {
let decoder = JSONDecoder()

return Just(data)
.decode(type: T.self, decoder: decoder)
.mapError({ .parse(description: $0.localizedDescription) })
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension URLRequest {
init(_ endpoint: APIEndpoint,
_ method: APIMethod,
_ parameters: [String: Any?]? = nil) {
let urlString = "\(URLRequest.baseUrl)\(endpoint.path())"
let urlString = "\(URLRequest.baseUrl)\(endpoint.path)"
let url = URL(string: urlString)!
self.init(url: url)
self.httpMethod = method.rawValue
Expand Down
2 changes: 1 addition & 1 deletion SwiftRorty.iOS/data/model/dto/CharacterDto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ struct CharacterDto: Identifiable {
var isFavorite: Bool = false

static func defaultDto() -> CharacterDto {
return CharacterDto(created: nil, episode: nil, gender: nil, id: 1, image: nil, location: nil, name: "Rick Sanchez", origin: nil, species: "Human", status: Status.Alive, type: nil, url: nil)
return CharacterDto(created: nil, episode: nil, gender: nil, id: 1, image: nil, location: nil, name: "Rick Sanchez", origin: nil, species: "Human", status: Status.alive, type: nil, url: nil)
}
}
13 changes: 13 additions & 0 deletions SwiftRorty.iOS/data/model/dto/CharacterListDto.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// CharacterListDto.swift
// SwiftRorty.iOS
//
// Created by developersancho on 27.02.2022.
//

import Foundation

struct CharacterListDto {
let info: PageInfo
let characters: [CharacterDto]
}
4 changes: 2 additions & 2 deletions SwiftRorty.iOS/data/model/dto/DtoExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import Foundation
import SwiftUI

extension CharacterResponse {
func toCharacterDtoList() -> [CharacterDto]? {
results?.map { $0.toCharacterDto() }
func toCharacterDtoList() -> [CharacterDto] {
results.map { $0.toCharacterDto() }
}
}

Expand Down
4 changes: 2 additions & 2 deletions SwiftRorty.iOS/data/model/remote/base/PageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import Foundation

struct PageInfo: Codable {
let count: Int?
let count: Int
let next: String?
let pages: Int?
let pages: Int
let prev: String?

enum CodingKeys: String, CodingKey {
Expand Down
6 changes: 3 additions & 3 deletions SwiftRorty.iOS/data/model/remote/base/Status.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

enum Status: String, Codable {
case Alive
case Dead
case Unknown
case alive = "Alive"
case dead = "Dead"
case unknown = "unknown"
}
15 changes: 15 additions & 0 deletions SwiftRorty.iOS/data/model/remote/character/CharacterInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,19 @@ struct CharacterInfo: Codable {
let status: Status?
let type: String?
let url: String?

enum CodingKeys: String, CodingKey {
case created = "created"
case episode = "episode"
case gender = "gender"
case id = "id"
case image = "image"
case location = "location"
case name = "name"
case origin = "origin"
case species = "species"
case status = "status"
case type = "type"
case url = "url"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import Foundation

struct CharacterResponse: Codable {
let pageInfo: PageInfo?
let results: [CharacterInfo]?
let pageInfo: PageInfo
let results: [CharacterInfo]

enum CodingKeys: String, CodingKey {
case pageInfo = "info"
case results = "results"
}
}
5 changes: 5 additions & 0 deletions SwiftRorty.iOS/data/model/remote/character/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ import Foundation
struct Location: Codable {
let name: String?
let url: String?

enum CodingKeys: String, CodingKey {
case name = "name"
case url = "url"
}
}
Loading

0 comments on commit 9ebeda8

Please sign in to comment.