diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/Models/Country.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/Models/Country.swift new file mode 100644 index 0000000..4d034ee --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/Models/Country.swift @@ -0,0 +1,27 @@ +// +// Country.swift +// SOLARAPI +// +// Created by Viktoriia Kostyleva on 03.10.2022. +// + +import Foundation + +public struct Country: Codable { + public let code: String + public let nodesCount: Int + + public init(code: String, nodesCount: Int) { + self.code = code + self.nodesCount = nodesCount + } +} + +// MARK: - Codable implementation + +public extension Country { + enum CodingKeys: String, CodingKey { + case code + case nodesCount = "nodes_count" + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift index f7ce7d5..372ba60 100644 --- a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift @@ -11,6 +11,7 @@ import Alamofire enum NodesAPITarget { case getNodes(GetNodesRequest) case postNodesByAddress(PostNodesByAddressRequest) + case getCountries } extension NodesAPITarget: APITarget { @@ -20,6 +21,8 @@ extension NodesAPITarget: APITarget { return .get case .postNodesByAddress: return .post + case .getCountries: + return .get } } @@ -29,6 +32,8 @@ extension NodesAPITarget: APITarget { return "dvpn/getNodes" case .postNodesByAddress: return "dvpn/postNodesByAddress" + case .getCountries: + return "dvpn/getCountries" } } @@ -38,6 +43,8 @@ extension NodesAPITarget: APITarget { return .requestParameters(parameters: request.dictionary ?? [:], encoding: URLEncoding.default) case let .postNodesByAddress(request): return .requestJSONEncodable(request) + case .getCountries: + return .requestPlain } } } diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProvider.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProvider.swift index 0d66064..aeac5d3 100644 --- a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProvider.swift +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProvider.swift @@ -44,6 +44,13 @@ extension NodesProvider: NodesProviderType { .validate() .responseDecodable(completionHandler: getResponseHandler(completion: completion)) } + + public func getCountries(completion: @escaping (Result<[Country], NetworkError>) -> Void) { + AF + .request(request(for: .getCountries)) + .validate() + .responseDecodable(completionHandler: getResponseHandler(completion: completion)) + } } private extension NodesProvider { diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProviderType.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProviderType.swift index 8ba8630..92c1037 100644 --- a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProviderType.swift +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesProviderType.swift @@ -18,4 +18,6 @@ public protocol NodesProviderType { _ postNodesRequest: PostNodesByAddressRequest, completion: @escaping (Result, NetworkError>) -> Void ) + + func getCountries(completion: @escaping (Result<[Country], NetworkError>) -> Void) } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj index 794c93b..1c455dd 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj @@ -64,6 +64,8 @@ 923C372028E71D19003CFC03 /* PostNodesByAddressRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C371F28E71D19003CFC03 /* PostNodesByAddressRequest.swift */; }; 923C372228E72C87003CFC03 /* NodesByAddressPostBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C372128E72C87003CFC03 /* NodesByAddressPostBody.swift */; }; 923C372628EAE93E003CFC03 /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C372528EAE93E003CFC03 /* Encoder.swift */; }; + 923C372828EAF0E8003CFC03 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C372728EAF0E8003CFC03 /* Country.swift */; }; + 923C372A28EAFAA5003CFC03 /* NodesServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C372928EAFAA5003CFC03 /* NodesServiceError.swift */; }; 92D6B3FD28E19E20004CF9DF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B3FC28E19E20004CF9DF /* AppDelegate.swift */; }; 92D6B3FF28E19E20004CF9DF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B3FE28E19E20004CF9DF /* SceneDelegate.swift */; }; 92D6B40128E19E20004CF9DF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B40028E19E20004CF9DF /* ViewController.swift */; }; @@ -195,6 +197,8 @@ 923C371F28E71D19003CFC03 /* PostNodesByAddressRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostNodesByAddressRequest.swift; sourceTree = ""; }; 923C372128E72C87003CFC03 /* NodesByAddressPostBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesByAddressPostBody.swift; sourceTree = ""; }; 923C372528EAE93E003CFC03 /* Encoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = ""; }; + 923C372728EAF0E8003CFC03 /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; + 923C372928EAFAA5003CFC03 /* NodesServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesServiceError.swift; sourceTree = ""; }; 92D6B3F928E19E20004CF9DF /* SOLARdVPNCommunityCoreiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SOLARdVPNCommunityCoreiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 92D6B3FC28E19E20004CF9DF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 92D6B3FE28E19E20004CF9DF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -448,6 +452,7 @@ children = ( 92D6B47228E47EC9004CF9DF /* Models */, 92D6B46E28E47E2E004CF9DF /* NodesService.swift */, + 923C372928EAFAA5003CFC03 /* NodesServiceError.swift */, 92D6B47028E47E71004CF9DF /* NodesServiceType.swift */, ); path = NodesService; @@ -583,6 +588,7 @@ 92D6B45B28E46350004CF9DF /* Models */ = { isa = PBXGroup; children = ( + 923C372728EAF0E8003CFC03 /* Country.swift */, 92D6B45C28E4636B004CF9DF /* GetNodesRequest.swift */, 92D6B46228E47240004CF9DF /* Node.swift */, 92D6B46028E463EF004CF9DF /* NodeStatusType.swift */, @@ -889,6 +895,7 @@ 22C3EEAA28E4733C007DB01B /* NETunnelProviderProtocol+Extension.swift in Sources */, 22488AA328E72C4E00FE29C3 /* StoresWallet.swift in Sources */, 22C3EED128E48B52007DB01B /* TunnelStatus.swift in Sources */, + 923C372A28EAFAA5003CFC03 /* NodesServiceError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -905,6 +912,7 @@ 92D6B46328E47240004CF9DF /* Node.swift in Sources */, 92D6B45F28E4639D004CF9DF /* OrderType.swift in Sources */, 92D6B45228E45F03004CF9DF /* NodesProvider.swift in Sources */, + 923C372828EAF0E8003CFC03 /* Country.swift in Sources */, 92D6B44628E34D7C004CF9DF /* APIRequest.swift in Sources */, 92D6B42828E32900004CF9DF /* SOLARAPI.docc in Sources */, 92D6B45A28E46283004CF9DF /* NetworkError.swift in Sources */, diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift index 5777022..ddad30b 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift @@ -11,20 +11,23 @@ import SOLARAPI protocol NoContext {} final class CommonContext { + // Providers + let nodesProvider: NodesProviderType + + // Services let nodesService: NodesServiceType - let tunnelManager: TunnelManagerType init( - nodesService: NodesServiceType, - tunnelManager: TunnelManagerType + nodesProvider: NodesProviderType, + nodesService: NodesServiceType ) { + self.nodesProvider = nodesProvider self.nodesService = nodesService - self.tunnelManager = tunnelManager } } +protocol HasNodesProvider { var nodesProvider: NodesProviderType { get } } +extension CommonContext: HasNodesProvider {} + protocol HasNodesService { var nodesService: NodesServiceType { get } } extension CommonContext: HasNodesService {} - -protocol HasTunnelManager { var tunnelManager: TunnelManagerType { get } } -extension CommonContext: HasTunnelManager {} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift index 3799775..96bf911 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift @@ -11,17 +11,13 @@ import SOLARAPI /// This class should configure all required services and inject them into a Context final class ContextBuilder { func buildContext() -> CommonContext { - let generalSettingsStorage = GeneralSettingsStorage() - let nodesProvider = NodesProvider(configuration: .init(baseURL: ClientConstants.backendURL)) let nodesService = NodesService(nodesProvider: nodesProvider) - let tunnelManager = TunnelManager(storage: generalSettingsStorage) - return CommonContext( - nodesService: nodesService, - tunnelManager: tunnelManager + nodesProvider: nodesProvider, + nodesService: nodesService ) } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/NodesRouteCollection.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/NodesRouteCollection.swift index ed56e36..4abf670 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/NodesRouteCollection.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/NodesRouteCollection.swift @@ -6,13 +6,15 @@ // import Vapor +import SOLARAPI struct NodesRouteCollection: RouteCollection { - let context: HasNodesService + let context: HasNodesProvider func boot(routes: RoutesBuilder) throws { - routes.get("nodes", use: getNodes) + routes.get("nodes", use: getNodes) routes.post("nodesByAddress", use: postNodesByAddress) + routes.get("countries", use: getCountries) } } @@ -26,23 +28,56 @@ extension NodesRouteCollection { let query = req.query[String.self, at: GetNodesRequest.CodingKeys.query.rawValue] let page = req.query[Int.self, at: GetNodesRequest.CodingKeys.page.rawValue] - return try await context.nodesService.loadNodes( - continentCode: continentCode, - countryCode: countryCode, - minPrice: minPrice, - maxPrice: maxPrice, - orderBy: orderBy, - query: query, - page: page - ) + return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + context.nodesProvider.getNodes( + .init( + status: .active, + continentCode: continentCode, + countryCode: countryCode, + minPrice: minPrice, + maxPrice: maxPrice, + orderBy: orderBy, + query: query, + page: page + ) + ) { result in + switch result { + case let .success(response): + Encoder.encode(model: response, continuation: continuation) + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: error.code), reason: error.localizedDescription)) + } + } + }) } func postNodesByAddress(_ req: Request) async throws -> String { let body = try req.content.decode(NodesByAddressPostBody.self) - return try await context.nodesService.getNodes( - by: body.blockchain_addresses, - page: body.page - ) + return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + context.nodesProvider.postNodesByAddress( + .init(addresses: body.blockchain_addresses, page: body.page) + ) { result in + switch result { + case let .success(response): + Encoder.encode(model: response, continuation: continuation) + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: error.code), reason: error.localizedDescription)) + } + } + }) + } + + func getCountries(_ req: Request) async throws -> String { + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + context.nodesProvider.getCountries() { result in + switch result { + case let .success(response): + Encoder.encode(model: response, continuation: continuation) + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: error.code), reason: error.localizedDescription)) + } + } + }) } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesService.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesService.swift index a8aec22..c9eff2a 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesService.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesService.swift @@ -7,11 +7,12 @@ import Foundation import SOLARAPI -import Vapor final class NodesService { private let nodesProvider: NodesProviderType + private var loadedNodes: [Node] = [] + init( nodesProvider: NodesProviderType ) { @@ -22,51 +23,61 @@ final class NodesService { // MARK: - NodesServiceType extension NodesService: NodesServiceType { + var nodes: [Node] { + loadedNodes + } + + /// Loads active nodes and saves them to local variable func loadNodes( - continentCode: String?, + continent: Continent?, countryCode: String?, minPrice: Int?, maxPrice: Int?, orderBy: OrderType?, query: String?, - page: Int? - ) async throws -> String { - try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in - nodesProvider.getNodes( - .init( - status: .active, - continentCode: continentCode, - countryCode: countryCode, - minPrice: minPrice, - maxPrice: maxPrice, - orderBy: orderBy, - query: query, - page: page - ) - ) { result in - switch result { - case let .success(response): - Encoder.encode(model: response, continuation: continuation) - case let .failure(error): - continuation.resume(throwing: Abort(.init(statusCode: error.code), reason: error.localizedDescription)) - } + page: Int?, + completion: @escaping (Result, Error>) -> Void + ) { + nodesProvider.getNodes( + .init( + status: .active, + continentCode: continent?.code, + countryCode: countryCode, + minPrice: minPrice, + maxPrice: maxPrice, + orderBy: orderBy, + query: query, + page: page + ) + ) { [weak self] result in + switch result { + case .failure(let error): + log.error(error) + completion(.failure(NodesServiceError.failToLoadData)) + case .success(let response): + completion(.success(response)) + self?.loadedNodes = response.data } - }) + } } - func getNodes( - by addresses: [String], - page: Int? - ) async throws -> String { - try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in - nodesProvider.postNodesByAddress(.init(addresses: addresses, page: page)) { result in - switch result { - case let .success(response): - Encoder.encode(model: response, continuation: continuation) - case let .failure(error): - continuation.resume(throwing: Abort(.init(statusCode: error.code), reason: error.localizedDescription)) + func getNode( + by address: String, + completion: @escaping (Result) -> Void + ) { + nodesProvider.postNodesByAddress(.init(addresses: [address], page: nil)) { result in + switch result { + case .failure(let error): + log.error(error) + completion(.failure(NodesServiceError.failToLoadData)) + case .success(let response): + guard let node = response.data.first else { + completion(.failure(NodesServiceError.failToLoadData)) + return } + + completion(.success(node)) } - }) + } } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceError.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceError.swift new file mode 100644 index 0000000..fb77f1b --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceError.swift @@ -0,0 +1,12 @@ +// +// NodesServiceError.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Viktoriia Kostyleva on 03.10.2022. +// + +import Foundation + +enum NodesServiceError: LocalizedError { + case failToLoadData +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceType.swift index c399da5..720dab2 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceType.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/NodesServiceType.swift @@ -9,18 +9,16 @@ import Foundation import SOLARAPI protocol NodesServiceType { + var nodes: [Node] { get } func loadNodes( - continentCode: String?, + continent: Continent?, countryCode: String?, minPrice: Int?, maxPrice: Int?, orderBy: OrderType?, query: String?, - page: Int? - ) async throws -> String - - func getNodes( - by: [String], - page: Int? - ) async throws -> String + page: Int?, + completion: @escaping (Result, Error>) -> Void + ) + func getNode(by: String, completion: @escaping (Result) -> Void) }