Merge branch 'refactor-infrastructures'

This commit is contained in:
Davide De Rosa 2019-04-25 23:59:44 +02:00
commit c1d8233339
22 changed files with 198 additions and 250 deletions

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- More infrastructure metadata.
## 1.6.0 Beta 1757 (2019-04-25)
### Changed

View File

@ -169,7 +169,7 @@ extension Infrastructure.Name {
}
}
extension Pool {
extension PoolGroup {
var logo: UIImage? {
return ImageAsset(name: country.lowercased()).image
}

View File

@ -33,19 +33,25 @@ protocol ProviderPoolViewControllerDelegate: class {
class ProviderPoolViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var models: [PoolModel] = []
private var categories: [PoolCategory] = []
private var sortedGroupsByCategory: [String: [PoolGroup]] = [:]
private var currentPool: Pool?
weak var delegate: ProviderPoolViewControllerDelegate?
func setModels(_ models: [PoolModel], currentPoolId: String?) {
self.models = models
func setInfrastructure(_ infrastructure: Infrastructure, currentPoolId: String?) {
categories = infrastructure.categories.sorted { $0.name.lowercased() < $1.name.lowercased() }
for c in categories {
sortedGroupsByCategory[c.name] = c.groups.values.sorted()
}
// XXX: uglyyy
for m in models {
for pools in m.poolsByGroup.values {
for p in pools {
for cat in categories {
for group in cat.groups.values {
for p in group.pools {
if p.id == currentPoolId {
currentPool = p
return
@ -78,12 +84,16 @@ class ProviderPoolViewController: UIViewController {
extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate {
private var selectedIndexPath: IndexPath? {
for (i, model) in models.enumerated() {
for entries in model.poolsByGroup.enumerated() {
guard let _ = entries.element.value.firstIndex(where: { $0.id == currentPool?.id }) else {
for (i, cat) in categories.enumerated() {
guard let sortedGroups = sortedGroupsByCategory[cat.name] else {
continue
}
for entries in cat.groups.enumerated() {
let group = entries.element.value
guard let _ = group.pools.firstIndex(where: { $0.id == currentPool?.id }) else {
continue
}
guard let row = model.sortedGroups.firstIndex(of: entries.element.key) else {
guard let row = sortedGroups.firstIndex(where: { $0.country == group.country && $0.area == group.area }) else {
continue
}
return IndexPath(row: row, section: i)
@ -93,48 +103,44 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
func numberOfSections(in tableView: UITableView) -> Int {
return models.count
return categories.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard models.count > 1 else {
guard categories.count > 1 else {
return nil
}
let model = models[section]
return model.isFree ? L10n.Provider.Pool.Sections.Free.header : L10n.Provider.Pool.Sections.Paid.header
let model = categories[section]
return model.name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let model = models[section]
return model.sortedGroups.count
let model = categories[section]
return model.groups.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.first else {
let group = poolGroup(at: indexPath)
guard let pool = group.pools.first else {
fatalError("Empty pools in group \(group)")
}
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.imageView?.image = pool.logo
cell.imageView?.image = group.logo
cell.leftText = pool.localizedCountry
if groupPools.count > 1 {
if group.pools.count > 1 {
cell.rightText = pool.area?.uppercased()
cell.accessoryType = .detailDisclosureButton // no checkmark!
} else {
cell.rightText = pool.areaId?.uppercased()
cell.rightText = pool.secondaryId
}
cell.isTappable = true
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.randomElement() else {
let group = poolGroup(at: indexPath)
guard let pool = group.pools.randomElement() else {
fatalError("Empty pools in group \(group)")
}
currentPool = pool
@ -142,32 +148,38 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.first else {
fatalError("Empty pools in group \(group)")
}
guard groupPools.count > 1 else {
let group = poolGroup(at: indexPath)
guard group.pools.count > 1 else {
return
}
let vc = OptionViewController<Pool>()
vc.title = pool.localizedCountry
vc.options = groupPools.sorted {
guard let lnum = $0.num, let ln = Int(lnum) else {
vc.title = group.localizedCountry
vc.options = group.pools.sorted {
guard let lnum = $0.num else {
return true
}
guard let rnum = $1.num, let rn = Int(rnum) else {
guard let rnum = $1.num else {
return false
}
return ln < rn
guard lnum != rnum else {
return $0.secondaryId < $1.secondaryId
}
return lnum < rnum
}
vc.selectedOption = currentPool
vc.descriptionBlock = { $0.areaId ?? "" } // XXX: fail gracefully
vc.descriptionBlock = { $0.secondaryId }
vc.selectionBlock = {
self.currentPool = $0
self.delegate?.providerPoolController(self, didSelectPool: $0)
}
navigationController?.pushViewController(vc, animated: true)
}
private func poolGroup(at indexPath: IndexPath) -> PoolGroup {
let model = categories[indexPath.section]
guard let sortedGroups = sortedGroupsByCategory[model.name] else {
fatalError("Missing sorted groups for category '\(model.name)'")
}
return sortedGroups[indexPath.row]
}
}

View File

@ -152,7 +152,7 @@ class ServiceViewController: UIViewController, TableModelHost {
case .providerPoolSegueIdentifier:
let vc = destination as? ProviderPoolViewController
vc?.setModels(InfrastructureCache.shared.poolModels(for: uncheckedProviderProfile), currentPoolId: uncheckedProviderProfile.poolId)
vc?.setInfrastructure(uncheckedProviderProfile.infrastructure, currentPoolId: uncheckedProviderProfile.poolId)
vc?.delegate = self
case .endpointSegueIdentifier:
@ -299,9 +299,6 @@ class ServiceViewController: UIViewController, TableModelHost {
}
self.lastInfrastructureUpdate = response.1
self.tableView.reloadData()
// invalidate current pool cache
InfrastructureCache.shared.removePoolModels(for: name)
}
if !isUpdating {
hud.hide()

View File

@ -89,7 +89,7 @@ class ShortcutsConnectToViewController: UITableViewController, ProviderPoolViewC
guard let provider = selectedProfile as? ProviderConnectionProfile else {
return
}
vc.setModels(InfrastructureCache.shared.poolModels(for: provider), currentPoolId: nil)
vc.setInfrastructure(provider.infrastructure, currentPoolId: nil)
vc.delegate = self
}

View File

@ -78,8 +78,7 @@
0E57F64620C83FC7008323CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64420C83FC7008323CF /* LaunchScreen.storyboard */; };
0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; };
0E58BF65224152F9006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; };
0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolModel.swift */; };
0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */; };
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolCategory.swift */; };
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; };
0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; };
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; };
@ -223,8 +222,7 @@
0E5E5DDE215119AF00E318A3 /* VPNStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = "<group>"; };
0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = "<group>"; };
0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = "<group>"; };
0E66A26F225FE25800F9C779 /* PoolModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolModel.swift; sourceTree = "<group>"; };
0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructureCache.swift; sourceTree = "<group>"; };
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.swift; sourceTree = "<group>"; };
0E6BE13920CFB76800A6DD36 /* ApplicationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationError.swift; sourceTree = "<group>"; };
0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogViewController.swift; sourceTree = "<group>"; };
0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; };
@ -484,9 +482,7 @@
isa = PBXGroup;
children = (
0EBE3AA3213DC1B000BFA2F5 /* HostConnectionProfile.swift */,
0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */,
0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */,
0E66A26F225FE25800F9C779 /* PoolModel.swift */,
0E79D14021919F5600BB5FB2 /* ProfileKey.swift */,
0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */,
);
@ -529,6 +525,7 @@
0EBE3A83213C6ADE00BFA2F5 /* InfrastructureFactory.swift */,
0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */,
0ED31C0F20CF09A30027975F /* Pool.swift */,
0E66A26F225FE25800F9C779 /* PoolCategory.swift */,
0E533B152258E03B00EF94FC /* PoolGroup.swift */,
0E39BCEF214B9EF10035E9DE /* WebServices.swift */,
);
@ -1016,7 +1013,7 @@
files = (
0E3152BD223FA03D00F61841 /* GroupConstants.swift in Sources */,
0ECEB10A224FECEA00E9E551 /* DataUnit.swift in Sources */,
0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */,
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */,
0E3152C2223FA04800F61841 /* MockVPNProvider.swift in Sources */,
0E533B162258E03B00EF94FC /* PoolGroup.swift in Sources */,
0E3152D2223FA05400F61841 /* DebugLog.swift in Sources */,
@ -1047,7 +1044,6 @@
0E3152CD223FA05400F61841 /* ConnectionProfile.swift in Sources */,
0E3152BC223FA03D00F61841 /* ApplicationError.swift in Sources */,
0E3152C9223FA04D00F61841 /* InfrastructureFactory.swift in Sources */,
0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */,
0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */,
0E3152D3223FA05400F61841 /* EndpointDataSource.swift in Sources */,
0E3152D4223FA05400F61841 /* Preferences.swift in Sources */,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -153,9 +153,6 @@
"account.cells.open_guide.caption" = "See your credentials";
"account.cells.signup.caption" = "Register with %@";
"provider.pool.sections.free.header" = "Free";
"provider.pool.sections.paid.header" = "Paid";
"endpoint.sections.location_addresses.header" = "Addresses";
"endpoint.sections.location_protocols.header" = "Protocols";
"endpoint.cells.any_address.caption" = "Automatic";

View File

@ -47,7 +47,7 @@ public class AppConstants {
}
public class Web {
private static let version = "v1"
private static let version = "v2"
private static let baseURL = Repos.api.appendingPathComponent(version)

View File

@ -1,76 +0,0 @@
//
// InfrastructureCache.swift
// Passepartout
//
// Created by Davide De Rosa on 4/11/19.
// Copyright (c) 2019 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
// TODO: retain max N pool models at a time (LRU)
public class InfrastructureCache {
public static let shared = InfrastructureCache()
private var poolModelsByName: [Infrastructure.Name: [PoolModel]]
private init() {
poolModelsByName = [:]
}
public func poolModels(for provider: ProviderConnectionProfile) -> [PoolModel] {
if let models = poolModelsByName[provider.name] {
return models
}
let freeModel = PoolModel(isFree: true)
let paidModel = PoolModel(isFree: false)
for p in provider.infrastructure.pools {
if p.isFree ?? false {
freeModel.addPool(p)
} else {
paidModel.addPool(p)
}
// if p.id == currentPoolId {
// currentPool = p
// }
}
freeModel.sort()
paidModel.sort()
var models: [PoolModel] = []
if !freeModel.isEmpty {
models.append(freeModel)
}
if !paidModel.isEmpty {
models.append(paidModel)
}
poolModelsByName[provider.name] = models
return models
}
public func removePoolModels(for name: Infrastructure.Name? = nil) {
if let name = name {
poolModelsByName.removeValue(forKey: name)
return
}
poolModelsByName.removeAll()
}
}

View File

@ -1,5 +1,5 @@
//
// PoolModel.swift
// PoolCategory.swift
// Passepartout
//
// Created by Davide De Rosa on 4/11/19.
@ -25,34 +25,8 @@
import Foundation
public class PoolModel {
public let isFree: Bool
public struct PoolCategory: Codable {
public let name: String
public var poolsByGroup: [PoolGroup: [Pool]]
public private(set) var sortedGroups: [PoolGroup]
public init(isFree: Bool) {
self.isFree = isFree
poolsByGroup = [:]
sortedGroups = []
}
public var isEmpty: Bool {
return sortedGroups.isEmpty
}
public func addPool(_ p: Pool) {
let group = p.group()
if var existingPools = poolsByGroup[group] {
existingPools.append(p)
poolsByGroup[group] = existingPools
} else {
poolsByGroup[group] = [p]
}
}
public func sort() {
sortedGroups = poolsByGroup.keys.sorted()
}
public let groups: [String: PoolGroup]
}

View File

@ -167,12 +167,19 @@ public extension ProviderConnectionProfile {
public extension ProviderConnectionProfile {
var mainAddress: String? {
assert(pool != nil, "Getting provider main address but no pool set")
return pool?.hostname
guard let pool = pool else {
assertionFailure("Getting provider main address but no pool set")
return nil
}
return pool.hostname
}
var addresses: [String] {
return pool?.addresses() ?? []
var addrs = pool?.addresses() ?? []
if let pool = pool, let externalHostname = try? preset?.externalConfiguration(forKey: .hostname, infrastructureName: infrastructure.name, pool: pool) as? String {
addrs.insert(externalHostname, at: 0)
}
return addrs
}
var protocols: [EndpointProtocol] {

View File

@ -53,7 +53,7 @@ public struct Infrastructure: Codable {
public let name: Name
public let pools: [Pool]
public let categories: [PoolCategory]
public let presets: [InfrastructurePreset]
@ -69,11 +69,27 @@ public struct Infrastructure: Codable {
}
public func pool(for identifier: String) -> Pool? {
return pools.first { $0.id == identifier }
for cat in categories {
for group in cat.groups.values {
guard let found = group.pools.first(where: { $0.id == identifier }) else {
continue
}
return found
}
}
return nil
}
public func pool(withPrefix prefix: String) -> Pool? {
return pools.first { $0.id.hasPrefix(prefix) }
for cat in categories {
for group in cat.groups.values {
guard let found = group.pools.first(where: { $0.id.hasPrefix(prefix) }) else {
continue
}
return found
}
}
return nil
}
public func preset(for identifier: String) -> InfrastructurePreset? {

View File

@ -38,6 +38,8 @@ public struct InfrastructurePreset: Codable {
case key
case wrapKeyData = "wrap.key.data"
case hostname
}
public enum PresetKeys: String, CodingKey {
@ -95,30 +97,52 @@ public struct InfrastructurePreset: Codable {
public func hasProtocol(_ proto: EndpointProtocol) -> Bool {
return configuration.sessionConfiguration.endpointProtocols?.firstIndex(of: proto) != nil
}
public func externalConfiguration(forKey key: ExternalKey, infrastructureName: Infrastructure.Name, pool: Pool) throws -> Any? {
guard let pattern = external?[key] else {
return nil
}
let baseURL = infrastructureName.externalURL
switch key {
case .ca:
let filename = pattern.replacingOccurrences(of: "${id}", with: pool.id)
let caURL = baseURL.appendingPathComponent(filename)
return CryptoContainer(pem: try String(contentsOf: caURL))
public func injectExternalConfiguration(_ configuration: inout TunnelKitProvider.ConfigurationBuilder, with name: Infrastructure.Name, pool: Pool) throws {
case .wrapKeyData:
let filename = pattern.replacingOccurrences(of: "${id}", with: pool.id)
let tlsKeyURL = baseURL.appendingPathComponent(filename)
let file = try String(contentsOf: tlsKeyURL)
return StaticKey(file: file, direction: .client)
case .hostname:
return pattern.replacingOccurrences(of: "${id}", with: pool.id)
default:
break
}
return nil
}
public func injectExternalConfiguration(_ configuration: inout TunnelKitProvider.ConfigurationBuilder, with infrastructureName: Infrastructure.Name, pool: Pool) throws {
guard let external = external, !external.isEmpty else {
return
}
let baseURL = name.externalURL
var sessionBuilder = configuration.sessionConfiguration.builder()
if let pattern = external[.ca] {
let filename = pattern.replacingOccurrences(of: "${id}", with: pool.id)
let caURL = baseURL.appendingPathComponent(filename)
sessionBuilder.ca = CryptoContainer(pem: try String(contentsOf: caURL))
if let _ = external[.ca] {
sessionBuilder.ca = try externalConfiguration(forKey: .ca, infrastructureName: infrastructureName, pool: pool) as? CryptoContainer
}
if let pattern = external[.wrapKeyData] {
let filename = pattern.replacingOccurrences(of: "${id}", with: pool.id)
let tlsKeyURL = baseURL.appendingPathComponent(filename)
if let _ = external[.wrapKeyData] {
if let dummyWrap = sessionBuilder.tlsWrap {
let file = try String(contentsOf: tlsKeyURL)
if let staticKey = StaticKey(file: file, direction: .client) {
if let staticKey = try externalConfiguration(forKey: .wrapKeyData, infrastructureName: infrastructureName, pool: pool) as? StaticKey {
sessionBuilder.tlsWrap = SessionProxy.TLSWrap(strategy: dummyWrap.strategy, key: staticKey)
}
}
}
if let _ = external[.hostname] {
sessionBuilder.hostname = try externalConfiguration(forKey: .hostname, infrastructureName: infrastructureName, pool: pool) as? String
}
configuration.sessionConfiguration = sessionBuilder.build()
}

View File

@ -26,19 +26,19 @@
import Foundation
import TunnelKit
public struct Pool: Codable, Hashable, CustomStringConvertible {
public struct Pool: Codable, Hashable {
public enum CodingKeys: String, CodingKey {
case id
case name
case country
case extraCountries = "extra_countries"
case area
case num
case isFree = "free"
case tags
// case location
@ -49,29 +49,15 @@ public struct Pool: Codable, Hashable, CustomStringConvertible {
public let id: String
private let name: String
public let country: String
public let extraCountries: [String]?
public let area: String?
public let num: Int?
public let num: String?
public var areaId: String? {
let id: String
if let area = area, let num = num {
id = "\(area) #\(num)"
} else if let area = area {
id = area
} else if let num = num {
id = "#\(num)"
} else {
return nil
}
return id.uppercased()
}
public let isFree: Bool?
public let tags: [String]?
// public let location: (Double, Double)
@ -98,42 +84,49 @@ public struct Pool: Codable, Hashable, CustomStringConvertible {
return addrs
}
public func group() -> PoolGroup {
return PoolGroup(country: country, area: area)
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
// MARK: CustomStringConvertible
public var description: String {
return "{[\(id)] \"\(name)\"}"
}
}
extension Pool {
private static let localizedFormat = "%@ - %@"
public var localizedCountry: String {
return Utils.localizedCountry(country)
}
public var localizedId: String {
let countryString = localizedCountry
let zone: String
if let area = area, let num = num {
zone = "\(area) #\(num)"
} else if let area = area {
zone = area
} else if let num = num {
zone = "#\(num)"
} else {
return countryString
var comps: [String] = [localizedCountry]
if let secondaryId = optionalSecondaryId {
comps.append(secondaryId)
}
return String.init(format: Pool.localizedFormat, countryString, zone.uppercased())
return comps.joined(separator: " - ")
}
public var secondaryId: String {
return optionalSecondaryId ?? ""
}
private var optionalSecondaryId: String? {
var comps: [String] = []
if let extraCountries = extraCountries {
comps.append(contentsOf: extraCountries.map { Utils.localizedCountry($0) })
}
if let area = area {
comps.append(area.uppercased())
}
if let num = num {
comps.append("#\(num)")
}
guard !comps.isEmpty else {
return nil
}
var str = comps.joined(separator: " ")
if let tags = tags {
let suffix = tags.map { $0.uppercased() }.joined(separator: ",")
str = "\(str) (\(suffix))"
}
return str
}
}

View File

@ -25,31 +25,27 @@
import Foundation
public struct PoolGroup: Hashable, Comparable, CustomStringConvertible {
public struct PoolGroup: Codable, Hashable, Comparable, CustomStringConvertible {
public let country: String
public let area: String?
private let id: String
public let pools: [Pool]
private let localizedId: String
public init(country: String, area: String?) {
self.country = country
self.area = area
private var id: String {
var id = country
var localizedId = Utils.localizedCountry(country)
if let area = area {
id += area
localizedId += area
}
self.id = id
self.localizedId = localizedId
return id
}
public func contains(_ pool: Pool) -> Bool {
return (pool.country == country) && (pool.area == area)
private var localizedId: String {
var localizedId = Utils.localizedCountry(country)
if let area = area {
localizedId += area
}
return localizedId
}
// MARK: Hashable
@ -70,3 +66,9 @@ public struct PoolGroup: Hashable, Comparable, CustomStringConvertible {
return "{\(country), \(area ?? "--")}"
}
}
extension PoolGroup {
public var localizedCountry: String {
return Utils.localizedCountry(country)
}
}

View File

@ -1,5 +1,5 @@
#!/bin/sh
. .env.secret-deploy
SRC=$PROJECT_ROOT/api/v1
SRC=$PROJECT_ROOT/api/v2
DST=$PROJECT_ROOT/passepartout-ios/Passepartout/Resources/Web
rm -rf $DST && cp -pr $SRC $DST