Skip to content

Commit

Permalink
feat: Adding Repository and DataSource
Browse files Browse the repository at this point in the history
  • Loading branch information
xramos committed Apr 3, 2024
1 parent 672a29b commit ca606c8
Show file tree
Hide file tree
Showing 7 changed files with 609 additions and 0 deletions.
48 changes: 48 additions & 0 deletions MyComics/MyComics.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
objects = {

/* Begin PBXBuildFile section */
DA1500002BBD665400513DD7 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15FFFF2BBD665400513DD7 /* Repository.swift */; };
DA1500022BBD668700513DD7 /* RepositoryImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1500012BBD668700513DD7 /* RepositoryImplementation.swift */; };
DA1500042BBD66D000513DD7 /* RemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1500032BBD66D000513DD7 /* RemoteDataSource.swift */; };
DA1500072BBD688E00513DD7 /* URLProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1500062BBD688E00513DD7 /* URLProtocolMock.swift */; };
DA15000A2BBD68D500513DD7 /* RemoteDataSourceUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1500092BBD68D500513DD7 /* RemoteDataSourceUnitTests.swift */; };
DA15000C2BBD696800513DD7 /* RepositoryImplementationUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15000B2BBD696800513DD7 /* RepositoryImplementationUnitTests.swift */; };
DA15FFA72BBD5DC900513DD7 /* MyComicsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15FFA62BBD5DC900513DD7 /* MyComicsApp.swift */; };
DA15FFA92BBD5DC900513DD7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15FFA82BBD5DC900513DD7 /* ContentView.swift */; };
DA15FFAB2BBD5DCA00513DD7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA15FFAA2BBD5DCA00513DD7 /* Assets.xcassets */; };
Expand Down Expand Up @@ -50,6 +56,11 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
DA1500012BBD668700513DD7 /* RepositoryImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryImplementation.swift; sourceTree = "<group>"; };
DA1500032BBD66D000513DD7 /* RemoteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteDataSource.swift; sourceTree = "<group>"; };
DA1500062BBD688E00513DD7 /* URLProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLProtocolMock.swift; sourceTree = "<group>"; };
DA1500092BBD68D500513DD7 /* RemoteDataSourceUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteDataSourceUnitTests.swift; sourceTree = "<group>"; };
DA15000B2BBD696800513DD7 /* RepositoryImplementationUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryImplementationUnitTests.swift; sourceTree = "<group>"; };
DA15FFA32BBD5DC900513DD7 /* MyComics.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyComics.app; sourceTree = BUILT_PRODUCTS_DIR; };
DA15FFA62BBD5DC900513DD7 /* MyComicsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyComicsApp.swift; sourceTree = "<group>"; };
DA15FFA82BBD5DC900513DD7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -77,6 +88,7 @@
DA15FFF82BBD62E800513DD7 /* ServerImageModelUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerImageModelUnitTests.swift; sourceTree = "<group>"; };
DA15FFFA2BBD640200513DD7 /* ServerPowerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerPowerUnitTests.swift; sourceTree = "<group>"; };
DA15FFFC2BBD643B00513DD7 /* CharacterUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterUnitTests.swift; sourceTree = "<group>"; };
DA15FFFF2BBD665400513DD7 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -104,6 +116,23 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
DA1500052BBD688400513DD7 /* Mocks */ = {
isa = PBXGroup;
children = (
DA1500062BBD688E00513DD7 /* URLProtocolMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
DA1500082BBD68B700513DD7 /* Repositories */ = {
isa = PBXGroup;
children = (
DA1500092BBD68D500513DD7 /* RemoteDataSourceUnitTests.swift */,
DA15000B2BBD696800513DD7 /* RepositoryImplementationUnitTests.swift */,
);
path = Repositories;
sourceTree = "<group>";
};
DA15FF9A2BBD5DC900513DD7 = {
isa = PBXGroup;
children = (
Expand All @@ -127,6 +156,7 @@
DA15FFA52BBD5DC900513DD7 /* MyComics */ = {
isa = PBXGroup;
children = (
DA15FFFE2BBD664600513DD7 /* Repositories */,
DA15FFD92BBD5FA900513DD7 /* Entitites */,
DA15FFD42BBD5EBE00513DD7 /* Enumerations */,
DA15FFD12BBD5E6E00513DD7 /* Network */,
Expand All @@ -151,6 +181,8 @@
DA15FFB72BBD5DCB00513DD7 /* MyComicsTests */ = {
isa = PBXGroup;
children = (
DA1500082BBD68B700513DD7 /* Repositories */,
DA1500052BBD688400513DD7 /* Mocks */,
DA15FFF12BBD626D00513DD7 /* Entities */,
);
path = MyComicsTests;
Expand Down Expand Up @@ -257,6 +289,16 @@
path = Entities;
sourceTree = "<group>";
};
DA15FFFE2BBD664600513DD7 /* Repositories */ = {
isa = PBXGroup;
children = (
DA15FFFF2BBD665400513DD7 /* Repository.swift */,
DA1500012BBD668700513DD7 /* RepositoryImplementation.swift */,
DA1500032BBD66D000513DD7 /* RemoteDataSource.swift */,
);
path = Repositories;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -392,9 +434,12 @@
DA15FFD82BBD5F3F00513DD7 /* Constants.swift in Sources */,
DA15FFD62BBD5EC800513DD7 /* HTTPError.swift in Sources */,
DA15FFA92BBD5DC900513DD7 /* ContentView.swift in Sources */,
DA1500042BBD66D000513DD7 /* RemoteDataSource.swift in Sources */,
DA15FFEE2BBD61C200513DD7 /* ServerCharacter.swift in Sources */,
DA15FFD32BBD5E8100513DD7 /* ComicsNetworkManager.swift in Sources */,
DA15FFE22BBD603400513DD7 /* ServerBaseResponse.swift in Sources */,
DA1500022BBD668700513DD7 /* RepositoryImplementation.swift in Sources */,
DA1500002BBD665400513DD7 /* Repository.swift in Sources */,
DA15FFDE2BBD5FD300513DD7 /* ServerImageModel.swift in Sources */,
DA15FFEA2BBD618200513DD7 /* Power.swift in Sources */,
DA15FFEC2BBD61AD00513DD7 /* Gender.swift in Sources */,
Expand All @@ -410,9 +455,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DA15000A2BBD68D500513DD7 /* RemoteDataSourceUnitTests.swift in Sources */,
DA1500072BBD688E00513DD7 /* URLProtocolMock.swift in Sources */,
DA15FFF92BBD62E800513DD7 /* ServerImageModelUnitTests.swift in Sources */,
DA15FFF72BBD628F00513DD7 /* ServerCharacterUnitTests.swift in Sources */,
DA15FFFB2BBD640200513DD7 /* ServerPowerUnitTests.swift in Sources */,
DA15000C2BBD696800513DD7 /* RepositoryImplementationUnitTests.swift in Sources */,
DA15FFFD2BBD643B00513DD7 /* CharacterUnitTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
90 changes: 90 additions & 0 deletions MyComics/MyComics/Repositories/RemoteDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// RemoteDataSource.swift
// MyComics
//
// Created by Xavier Ramos on 3/4/24.
//

import Foundation
import Combine

class RemoteDataSource {

// URLs

static let searchURL: String = "search/"
static let getCharacterURL: String = "character/"

// Variables

private let baseURLString: String
private let session: URLSession

// MARK: - Methods

init(baseURL: String = Constants.baseURL, session: URLSession = URLSession.shared) {

self.baseURLString = baseURL
self.session = session
}

func searchCharacter(value: String) -> AnyPublisher<ServerBaseArrayResponse<ServerCharacter>, Error> {

let networkManager = ComicsNetworkManager(baseURL: baseURLString, session: session)

let urlRequest = getSearchCharacterEndpoint(value: value)

return networkManager.performRequest(urlRequest: urlRequest)
}

func getCharacter(id: Int) -> AnyPublisher<ServerBaseResponse<ServerCharacter>, Error> {

let networkManager = ComicsNetworkManager(baseURL: baseURLString, session: session)

let urlRequest = getCharacterEndpoint(id: id)

return networkManager.performRequest(urlRequest: urlRequest)
}
}

// MARK: - Endpoints

extension RemoteDataSource {

func getSearchCharacterEndpoint(value: String) -> URLRequest {

let endpoint = "\(baseURLString)\(RemoteDataSource.searchURL)"

var components = URLComponents(string: endpoint)

let queryItems = [URLQueryItem(name: "api_key", value: Constants.apiKey),
URLQueryItem(name: "query", value: value),
URLQueryItem(name: "format", value: "json"),
URLQueryItem(name: "field_list", value: "id,image,name,aliases,real_name,gender"),
URLQueryItem(name: "resources", value: "character"),
URLQueryItem(name: "limit", value: "100" )]

components?.queryItems = queryItems

let urlRequest = URLRequest(url: (components?.url!)!)

return urlRequest
}

func getCharacterEndpoint(id: Int) -> URLRequest {

let endpoint = "\(baseURLString)\(RemoteDataSource.getCharacterURL)4005-\(id)"

var components = URLComponents(string: endpoint)

let queryItems = [URLQueryItem(name: "api_key", value: Constants.apiKey),
URLQueryItem(name: "format", value: "json"),
URLQueryItem(name: "field_list", value: "id,image,name,aliases,real_name,birth,deck,gender,origin,powers")]

components?.queryItems = queryItems

let urlRequest = URLRequest(url: (components?.url!)!)

return urlRequest
}
}
16 changes: 16 additions & 0 deletions MyComics/MyComics/Repositories/Repository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Repository.swift
// MyComics
//
// Created by Xavier Ramos on 3/4/24.
//

import Foundation
import Combine

protocol Repository {

func searchCharacter(value: String) -> AnyPublisher<[Character], Error>

func getCharacter(id: Int) -> AnyPublisher<Character, Error>
}
58 changes: 58 additions & 0 deletions MyComics/MyComics/Repositories/RepositoryImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// RepositoryImplementation.swift
// MyComics
//
// Created by Xavier Ramos on 3/4/24.
//

import Foundation
import Combine

class RepositoryImplementation {

private let remoteDataSource: RemoteDataSource

init(remoteDataSource: RemoteDataSource = RemoteDataSource()) {
self.remoteDataSource = remoteDataSource
}
}

// MARK: - Repository

extension RepositoryImplementation: Repository {

func searchCharacter(value: String) -> AnyPublisher<[Character], Error> {

return remoteDataSource.searchCharacter(value: value).map { serverCharacters -> [Character] in

var characters: [Character] = []

for serverCharacter in serverCharacters.results {

// convert to entity
let character = serverCharacter.converToEntity()

characters.append(character)
}

// Return
return characters
}
.mapError({ $0 })
.eraseToAnyPublisher()
}

func getCharacter(id: Int) -> AnyPublisher<Character, Error> {

return remoteDataSource.getCharacter(id: id).map { serverCharacter -> Character in

// convert to entity
let character = serverCharacter.results.converToEntity()

// Return
return character
}
.mapError({ $0 })
.eraseToAnyPublisher()
}
}
57 changes: 57 additions & 0 deletions MyComics/MyComicsTests/Mocks/URLProtocolMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// URLProtocolMock.swift
// MyComicsTests
//
// Created by Xavier Ramos on 3/4/24.
//

import Foundation

// https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
class URLProtocolMock: URLProtocol {

// this dictionary maps URLs to test data
static var testURLs = [URL?: Data]()
static var response: URLResponse?
static var error: Error?

// say we want to handle all types of request
override class func canInit(with request: URLRequest) -> Bool {

return true
}

// ignore this method; just send back what we were given
override class func canonicalRequest(for request: URLRequest) -> URLRequest {

return request
}

override func startLoading() {

// if we have a valid URL and if we have test data for that URL…
if let url = request.url, let data = URLProtocolMock.testURLs[url] {
// …load it immediately.
self.client?.urlProtocol(self, didLoad: data)
}

// …and we return our response if defined…
if let response = URLProtocolMock.response {
self.client?.urlProtocol(self,
didReceive: response,
cacheStoragePolicy: .notAllowed)
}
// …and we return our error if defined…
if let error = URLProtocolMock.error {
self.client?.urlProtocol(self, didFailWithError: error)
}

// mark that we've finished
self.client?.urlProtocolDidFinishLoading(self)
}

// this method is required but doesn't need to do anything
override func stopLoading() {
// Nothing to do
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// RemoteDataSourceUnitTests.swift
// MyComicsTests
//
// Created by Xavier Ramos on 3/4/24.
//

import XCTest
@testable import MyComics

final class RemoteDataSourceUnitTests: XCTestCase {

var sut: RemoteDataSource?

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
try super.setUpWithError()

sut = RemoteDataSource(baseURL: "http://jsonplaceholder.typicode.com/")
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
sut = nil

try super.tearDownWithError()
}

func testGetSearchCharacterEndpoint() {

// Given
let request = "search/"

// When
let response = sut!.getSearchCharacterEndpoint(value: request)

// Then
XCTAssertNotNil(response)
XCTAssertEqual(response.url?.absoluteString.split(separator: "?").first, "http://jsonplaceholder.typicode.com/\(request)")
}

func testCharacterEndpoint() {

// Given
let request = 1

// When
let response = sut!.getCharacterEndpoint(id: request)

// Then
XCTAssertNotNil(response)
XCTAssertEqual(response.url?.absoluteString.split(separator: "?").first, "http://jsonplaceholder.typicode.com/character/4005-\(request)")
}
}
Loading

0 comments on commit ca606c8

Please sign in to comment.