From 5502b33f55805536cb4fe7434121333b92ac79c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20K=C3=B6tte?= Date: Wed, 22 Sep 2021 00:05:22 -0700 Subject: [PATCH] Refactor to provide protocols for all public structs This helps with mocking these types when testing software depending on this library. --- Sources/Wealthsimple/TransactionError.swift | 38 ++-- ...ccount.swift => WealthsimpleAccount.swift} | 111 +++++----- .../{Asset.swift => WealthsimpleAsset.swift} | 60 +++--- .../Wealthsimple/WealthsimpleDownloader.swift | 12 +- ...ition.swift => WealthsimplePosition.swift} | 79 +++---- ...on.swift => WealthsimpleTransaction.swift} | 198 ++++++++++-------- 6 files changed, 268 insertions(+), 230 deletions(-) rename Sources/Wealthsimple/{Account.swift => WealthsimpleAccount.swift} (66%) rename Sources/Wealthsimple/{Asset.swift => WealthsimpleAsset.swift} (53%) rename Sources/Wealthsimple/{Position.swift => WealthsimplePosition.swift} (76%) rename Sources/Wealthsimple/{Transaction.swift => WealthsimpleTransaction.swift} (71%) diff --git a/Sources/Wealthsimple/TransactionError.swift b/Sources/Wealthsimple/TransactionError.swift index 14d5318..b15e84a 100644 --- a/Sources/Wealthsimple/TransactionError.swift +++ b/Sources/Wealthsimple/TransactionError.swift @@ -7,29 +7,25 @@ import Foundation -extension Transaction { - - /// Errors which can happen when retrieving a Transaction - public enum TransactionError: Error { - /// When no data is received from the HTTP request - case noDataReceived - /// When an HTTP error occurs - case httpError(error: String) - /// When the received data is not valid JSON - case invalidJson(error: String) - /// When the received JSON does not have the right type - case invalidJsonType(json: Any) - /// When the received JSON does not have all expected values - case missingResultParamenter(json: [String: Any]) - /// When the received JSON does have an unexpected value - case invalidResultParamenter(json: [String: Any]) - /// An error with the token occured - case tokenError(_ error: TokenError) - } - +/// Errors which can happen when retrieving a Transaction +public enum TransactionError: Error { + /// When no data is received from the HTTP request + case noDataReceived + /// When an HTTP error occurs + case httpError(error: String) + /// When the received data is not valid JSON + case invalidJson(error: String) + /// When the received JSON does not have the right type + case invalidJsonType(json: Any) + /// When the received JSON does not have all expected values + case missingResultParamenter(json: [String: Any]) + /// When the received JSON does have an unexpected value + case invalidResultParamenter(json: [String: Any]) + /// An error with the token occured + case tokenError(_ error: TokenError) } -extension Transaction.TransactionError: LocalizedError { +extension TransactionError: LocalizedError { public var errorDescription: String? { switch self { case .noDataReceived: diff --git a/Sources/Wealthsimple/Account.swift b/Sources/Wealthsimple/WealthsimpleAccount.swift similarity index 66% rename from Sources/Wealthsimple/Account.swift rename to Sources/Wealthsimple/WealthsimpleAccount.swift index 2bda234..b962be5 100644 --- a/Sources/Wealthsimple/Account.swift +++ b/Sources/Wealthsimple/WealthsimpleAccount.swift @@ -10,63 +10,72 @@ import Foundation import FoundationNetworking #endif -/// An Account at Wealthsimple -public struct Account { - - /// Errors which can happen when retrieving an Account - public enum AccountError: Error { - /// When no data is received from the HTTP request - case noDataReceived - /// When an HTTP error occurs - case httpError(error: String) - /// When the received data is not valid JSON - case invalidJson(error: String) - /// When the received JSON does not have the right type - case invalidJsonType(json: Any) - /// When the received JSON does not have all expected values - case missingResultParamenter(json: [String: Any]) - /// When the received JSON does have an unexpected value - case invalidResultParamenter(json: [String: Any]) - /// An error with the token occured - case tokenError(_ error: TokenError) - } +/// Errors which can happen when retrieving an Account +public enum AccountError: Error { + /// When no data is received from the HTTP request + case noDataReceived + /// When an HTTP error occurs + case httpError(error: String) + /// When the received data is not valid JSON + case invalidJson(error: String) + /// When the received JSON does not have the right type + case invalidJsonType(json: Any) + /// When the received JSON does not have all expected values + case missingResultParamenter(json: [String: Any]) + /// When the received JSON does have an unexpected value + case invalidResultParamenter(json: [String: Any]) + /// An error with the token occured + case tokenError(_ error: TokenError) +} - /// Type of the account - /// - /// Note: Currently only Canadian Accounts are supported - public enum AccountType: String { - /// Tax free savings account (CA) - case tfsa = "ca_tfsa" - /// Cash (chequing) account (CA) - case chequing = "ca_cash_msb" - /// Saving (CA) - case saving = "ca_cash" - /// Registered Retirement Savings Plan (CA) - case rrsp = "ca_rrsp" - /// Non-registered account (CA) - case nonRegistered = "ca_non_registered" - /// Non-registered crypto currency account (CA) - case nonRegisteredCrypto = "ca_non_registered_crypto" - /// Locked-in retirement account (CA) - case lira = "ca_lira" - /// Joint account (CA) - case joint = "ca_joint" - /// Registered Retirement Income Fund (CA) - case rrif = "ca_rrif" - /// Life Income Fund (CA) - case lif = "ca_lif" - } +/// Type of the account +/// +/// Note: Currently only Canadian Accounts are supported +public enum AccountType: String { + /// Tax free savings account (CA) + case tfsa = "ca_tfsa" + /// Cash (chequing) account (CA) + case chequing = "ca_cash_msb" + /// Saving (CA) + case saving = "ca_cash" + /// Registered Retirement Savings Plan (CA) + case rrsp = "ca_rrsp" + /// Non-registered account (CA) + case nonRegistered = "ca_non_registered" + /// Non-registered crypto currency account (CA) + case nonRegisteredCrypto = "ca_non_registered_crypto" + /// Locked-in retirement account (CA) + case lira = "ca_lira" + /// Joint account (CA) + case joint = "ca_joint" + /// Registered Retirement Income Fund (CA) + case rrif = "ca_rrif" + /// Life Income Fund (CA) + case lif = "ca_lif" +} - private static let url = URL(string: "https://api.production.wealthsimple.com/v1/accounts")! +/// An Account at Wealthsimple +public protocol Account { /// Type of the account - public let accountType: AccountType + var accountType: AccountType { get } /// Operating currency of the account - public let currency: String + var currency: String { get } /// Wealthsimple id for the account - public let id: String + var id: String { get } /// Number of the account - public let number: String + var number: String { get } + +} + +struct WealthsimpleAccount: Account { + + private static let url = URL(string: "https://api.production.wealthsimple.com/v1/accounts")! + + let accountType: AccountType + let currency: String + let id: String + let number: String private init(json: [String: Any]) throws { guard let id = json["id"] as? String, @@ -133,7 +142,7 @@ public struct Account { } var accounts = [Account]() for result in results { - accounts.append(try Account(json: result)) + accounts.append(try WealthsimpleAccount(json: result)) } completion(.success(accounts)) } catch { diff --git a/Sources/Wealthsimple/Asset.swift b/Sources/Wealthsimple/WealthsimpleAsset.swift similarity index 53% rename from Sources/Wealthsimple/Asset.swift rename to Sources/Wealthsimple/WealthsimpleAsset.swift index 68cc99e..387a385 100644 --- a/Sources/Wealthsimple/Asset.swift +++ b/Sources/Wealthsimple/WealthsimpleAsset.swift @@ -7,40 +7,46 @@ import Foundation -/// An asset, like a stock or a currency -public struct Asset { - - /// Errors which can happen when retrieving an Asset - public enum AssetError: Error { - /// When the received JSON does not have all expected values - case missingResultParamenter(json: [String: Any]) - /// When the received JSON does have an unexpected value - case invalidResultParamenter(json: [String: Any]) - } +/// Errors which can happen when retrieving an Asset +public enum AssetError: Error { + /// When the received JSON does not have all expected values + case missingResultParamenter(json: [String: Any]) + /// When the received JSON does have an unexpected value + case invalidResultParamenter(json: [String: Any]) +} - /// Type of the asset - public enum AssetType: String { - /// Cash - case currency - /// Equity - case equity - /// Mutal Funds - case mutualFund = "mutual_fund" - /// Bonds - case bond - /// ETFs - case exchangeTradedFund = "exchange_traded_fund" - } +/// Type of the asset +public enum AssetType: String { + /// Cash + case currency + /// Equity + case equity + /// Mutal Funds + case mutualFund = "mutual_fund" + /// Bonds + case bond + /// ETFs + case exchangeTradedFund = "exchange_traded_fund" +} +/// An asset, like a stock or a currency +public protocol Asset { /// Symbol of the asset, e.g. currency or ticker symbol - public let symbol: String + var symbol: String { get } /// Full name of the asset - public let name: String + var name: String { get } /// Currency the asset is held in - public let currency: String + var currency: String { get } /// Type of the asset, e.g. currency or ETF - public let type: AssetType + var type: AssetType { get } +} + +struct WealthsimpleAsset: Asset { + let symbol: String + let name: String + let currency: String + let type: AssetType let id: String init(json: [String: Any]) throws { diff --git a/Sources/Wealthsimple/WealthsimpleDownloader.swift b/Sources/Wealthsimple/WealthsimpleDownloader.swift index 952b9c1..3408e1e 100644 --- a/Sources/Wealthsimple/WealthsimpleDownloader.swift +++ b/Sources/Wealthsimple/WealthsimpleDownloader.swift @@ -81,12 +81,12 @@ public final class WealthsimpleDownloader { /// Get all Accounts the user has access to /// - Parameter completion: Result with an array of `Account`s or an `Account.AccountError` - public func getAccounts(completion: @escaping (Result<[Account], Account.AccountError>) -> Void) { + public func getAccounts(completion: @escaping (Result<[Account], AccountError>) -> Void) { guard let token = token else { completion(.failure(.tokenError(.noToken))) return } - Account.getAccounts(token: token) { + WealthsimpleAccount.getAccounts(token: token) { if case let .failure(error) = $0 { if case .tokenError = error { self.token = nil @@ -101,12 +101,12 @@ public final class WealthsimpleDownloader { /// - account: Account to retreive positions for /// - date: Date of which the positions should be downloaded. If not date is provided, not date is sent to the API. The API falls back to the current date. /// - completion: Result with an array of `Position`s or an `Position.PositionError` - public func getPositions(in account: Account, date: Date?, completion: @escaping (Result<[Position], Position.PositionError>) -> Void) { + public func getPositions(in account: Account, date: Date?, completion: @escaping (Result<[Position], PositionError>) -> Void) { guard let token = token else { completion(.failure(.tokenError(.noToken))) return } - Position.getPositions(token: token, account: account, date: date) { + WealthsimplePosition.getPositions(token: token, account: account, date: date) { if case let .failure(error) = $0 { if case .tokenError = error { self.token = nil @@ -121,12 +121,12 @@ public final class WealthsimpleDownloader { /// - account: Account to retreive transactions from /// - startDate: Date from which the transactions are downloaded. If not date is provided, not date is sent to the API. The API falls back to 30 days ago from today. /// - completion: Result with an array of `Transactions`s or an `Transactions.TransactionsError` - public func getTransactions(in account: Account, startDate: Date?, completion: @escaping (Result<[Transaction], Transaction.TransactionError>) -> Void) { + public func getTransactions(in account: Account, startDate: Date?, completion: @escaping (Result<[Transaction], TransactionError>) -> Void) { guard let token = token else { completion(.failure(.tokenError(.noToken))) return } - Transaction.getTransactions(token: token, account: account, startDate: startDate) { + WealthsimpleTransaction.getTransactions(token: token, account: account, startDate: startDate) { if case let .failure(error) = $0 { if case .tokenError = error { self.token = nil diff --git a/Sources/Wealthsimple/Position.swift b/Sources/Wealthsimple/WealthsimplePosition.swift similarity index 76% rename from Sources/Wealthsimple/Position.swift rename to Sources/Wealthsimple/WealthsimplePosition.swift index 1a2ee61..2536816 100644 --- a/Sources/Wealthsimple/Position.swift +++ b/Sources/Wealthsimple/WealthsimplePosition.swift @@ -10,28 +10,43 @@ import Foundation import FoundationNetworking #endif +/// Errors which can happen when retrieving a Position +public enum PositionError: Error { + /// When no data is received from the HTTP request + case noDataReceived + /// When an HTTP error occurs + case httpError(error: String) + /// When the received data is not valid JSON + case invalidJson(error: String) + /// When the received JSON does not have the right type + case invalidJsonType(json: Any) + /// When the received JSON does not have all expected values + case missingResultParamenter(json: [String: Any]) + /// When the received JSON does have an unexpected value + case invalidResultParamenter(json: [String: Any]) + /// An error with the assets occured + case assetError(_ error: AssetError) + /// An error with the token occured + case tokenError(_ error: TokenError) +} + /// A Position, like certain amount of a stock or a currency held in an account -public struct Position { +public protocol Position { + /// Wealthsimple identifier of the account in which this position is held + var accountId: String { get } + /// Asset which is held + var asset: Asset { get } + /// Number of units of the asset held + var quantity: String { get } + /// Price per pice of the asset on `priceDate` + var priceAmount: String { get } + /// Currency of the price + var priceCurrency: String { get } + /// Date of the positon + var positionDate: Date { get } +} - /// Errors which can happen when retrieving a Position - public enum PositionError: Error { - /// When no data is received from the HTTP request - case noDataReceived - /// When an HTTP error occurs - case httpError(error: String) - /// When the received data is not valid JSON - case invalidJson(error: String) - /// When the received JSON does not have the right type - case invalidJsonType(json: Any) - /// When the received JSON does not have all expected values - case missingResultParamenter(json: [String: Any]) - /// When the received JSON does have an unexpected value - case invalidResultParamenter(json: [String: Any]) - /// An error with the assets occured - case assetError(_ error: Asset.AssetError) - /// An error with the token occured - case tokenError(_ error: TokenError) - } +struct WealthsimplePosition: Position { private static let baseUrl = URLComponents(string: "https://api.production.wealthsimple.com/v1/positions")! @@ -41,18 +56,12 @@ public struct Position { return dateFormatter }() - /// Wealthsimple identifier of the account in which this position is held - public let accountId: String - /// Asset which is held - public let asset: Asset - /// Number of units of the asset held - public let quantity: String - /// Price per pice of the asset on `priceDate` - public let priceAmount: String - /// Currency of the price - public let priceCurrency: String - /// Date of the positon - public let positionDate: Date + let accountId: String + let asset: Asset + let quantity: String + let priceAmount: String + let priceCurrency: String + let positionDate: Date private init(json: [String: Any]) throws { guard let quantity = json["quantity"] as? String, @@ -71,9 +80,9 @@ public struct Position { throw PositionError.invalidResultParamenter(json: json) } do { - self.asset = try Asset(json: assetDict) + self.asset = try WealthsimpleAsset(json: assetDict) } catch { - throw PositionError.assetError(error as! Asset.AssetError) // swiftlint:disable:this force_cast + throw PositionError.assetError(error as! AssetError) // swiftlint:disable:this force_cast } self.accountId = accountId self.quantity = quantity @@ -138,7 +147,7 @@ public struct Position { } var positions = [Position]() for result in results { - positions.append(try Position(json: result)) + positions.append(try Self(json: result)) } completion(.success(positions)) } catch { diff --git a/Sources/Wealthsimple/Transaction.swift b/Sources/Wealthsimple/WealthsimpleTransaction.swift similarity index 71% rename from Sources/Wealthsimple/Transaction.swift rename to Sources/Wealthsimple/WealthsimpleTransaction.swift index 75a0be5..c253aa4 100644 --- a/Sources/Wealthsimple/Transaction.swift +++ b/Sources/Wealthsimple/WealthsimpleTransaction.swift @@ -10,111 +10,129 @@ import Foundation import FoundationNetworking #endif -/// A Transaction, like buying or selling stock -public struct Transaction { - - /// Type for the transaction, e.g. buying or selling - public enum TransactionType: String { - /// buying a Stock, ETF, ... - case buy - /// depositing cash in a registered account - case contribution - /// receiving a cash dividend - case dividend - /// custodian fee - case custodianFee - /// depositing cash in an unregistered account - case deposit - /// wealthsimple management fee - case fee - /// forex - case forex - /// grant - case grant - /// home buyers plan - case homeBuyersPlan - /// hst - case hst - /// charged interest - case chargedInterest - /// journal - case journal - /// US non resident withholding tax on dividend payments - case nonResidentWithholdingTax - /// redemption - case redemption - /// risk exposure fee - case riskExposureFee - /// refund - case refund - /// reimbursements, e.g. ETF Fee Rebates - case reimbursement - /// selling a Stock, ETF, ... - case sell - /// stock distribution - case stockDistribution - /// stock dividend - case stockDividend - /// transfer in - case transferIn - /// transfer out - case transferOut - /// withholding tax - case withholdingTax - /// withdrawal of cash - case withdrawal - /// Cash transfer into cash account - case paymentTransferIn = "wealthsimplePaymentsTransferIn" - /// Cash withdrawl from cash account - case paymentTransferOut = "wealthsimplePaymentsTransferOut" - /// Referral Bonus - case referralBonus - /// Interest paid in saving accounts - case interest - /// Wealthsimple Cash Card payments - case paymentSpend = "wealthsimplePaymentsSpend" - /// Wealthsimple Cash Cashback - case giveawayBonus - } - - private static let baseUrl = URLComponents(string: "https://api.production.wealthsimple.com/v1/transactions")! - - private static var dateFormatter: DateFormatter = { - var dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - return dateFormatter - }() +/// Type for the transaction, e.g. buying or selling +public enum TransactionType: String { + /// buying a Stock, ETF, ... + case buy + /// depositing cash in a registered account + case contribution + /// receiving a cash dividend + case dividend + /// custodian fee + case custodianFee + /// depositing cash in an unregistered account + case deposit + /// wealthsimple management fee + case fee + /// forex + case forex + /// grant + case grant + /// home buyers plan + case homeBuyersPlan + /// hst + case hst + /// charged interest + case chargedInterest + /// journal + case journal + /// US non resident withholding tax on dividend payments + case nonResidentWithholdingTax + /// redemption + case redemption + /// risk exposure fee + case riskExposureFee + /// refund + case refund + /// reimbursements, e.g. ETF Fee Rebates + case reimbursement + /// selling a Stock, ETF, ... + case sell + /// stock distribution + case stockDistribution + /// stock dividend + case stockDividend + /// transfer in + case transferIn + /// transfer out + case transferOut + /// withholding tax + case withholdingTax + /// withdrawal of cash + case withdrawal + /// Cash transfer into cash account + case paymentTransferIn = "wealthsimplePaymentsTransferIn" + /// Cash withdrawl from cash account + case paymentTransferOut = "wealthsimplePaymentsTransferOut" + /// Referral Bonus + case referralBonus + /// Interest paid in saving accounts + case interest + /// Wealthsimple Cash Card payments + case paymentSpend = "wealthsimplePaymentsSpend" + /// Wealthsimple Cash Cashback + case giveawayBonus +} +/// A Transaction, like buying or selling stock +public protocol Transaction { /// Wealthsimples identifier of this transaction - public let id: String + var id: String { get } /// Wealthsimple identifier of the account in which this transaction happend - public let accountId: String + var accountId: String { get } /// type of the transaction, like buy or sell - public let transactionType: TransactionType + var transactionType: TransactionType { get } /// description of the transaction - public let description: String + var description: String { get } /// symbol of the asset which is brought, sold, ... - public let symbol: String + var symbol: String { get } /// Number of units of the asset brought, sold, ... - public let quantity: String + var quantity: String { get } /// market pice of the asset - public let marketPriceAmount: String + var marketPriceAmount: String { get } /// Currency of the market price - public let marketPriceCurrency: String + var marketPriceCurrency: String { get } /// market value of the assets - public let marketValueAmount: String + var marketValueAmount: String { get } /// Currency of the market value - public let marketValueCurrency: String + var marketValueCurrency: String { get } /// Net chash change in the account - public let netCashAmount: String + var netCashAmount: String { get } /// Currency of the net cash change - public let netCashCurrency: String + var netCashCurrency: String { get } /// Foreign exchange rate applied - public let fxRate: String + var fxRate: String { get } /// Date when the trade was settled - public let effectiveDate: Date + var effectiveDate: Date { get } /// Date when the trade was processed - public let processDate: Date + var processDate: Date { get } +} + +struct WealthsimpleTransaction: Transaction { + + private static let baseUrl = URLComponents(string: "https://api.production.wealthsimple.com/v1/transactions")! + + private static var dateFormatter: DateFormatter = { + var dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + return dateFormatter + }() + + let id: String + let accountId: String + let transactionType: TransactionType + let description: String + let symbol: String + let quantity: String + let marketPriceAmount: String + let marketPriceCurrency: String + let marketValueAmount: String + let marketValueCurrency: String + let netCashAmount: String + let netCashCurrency: String + let fxRate: String + let effectiveDate: Date + let processDate: Date // swiftlint:disable:next function_body_length private init(json: [String: Any]) throws { @@ -221,7 +239,7 @@ public struct Transaction { } var transactions = [Transaction]() for result in results { - transactions.append(try Transaction(json: result)) + transactions.append(try WealthsimpleTransaction(json: result)) } completion(.success(transactions)) } catch {