From 04cc2dee1c21d414974622fae10097fb8bd5a9b8 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Wed, 22 Dec 2021 20:44:47 -0700 Subject: [PATCH] Copy code from Twigs iOS app --- Package.swift | 4 + README.md | 4 +- Sources/twigs/Budget.swift | 25 ++ Sources/twigs/Category.swift | 19 + Sources/twigs/RecurringTransaction.swift | 282 +++++++++++++++ Sources/twigs/Transaction.swift | 54 +++ Sources/twigs/TwigsApiService.swift | 442 +++++++++++++++++++++++ Sources/twigs/User.swift | 39 ++ Sources/twigs/twigs.swift | 6 - Tests/twigsTests/twigsTests.swift | 2 +- 10 files changed, 868 insertions(+), 9 deletions(-) create mode 100644 Sources/twigs/Budget.swift create mode 100644 Sources/twigs/Category.swift create mode 100644 Sources/twigs/RecurringTransaction.swift create mode 100644 Sources/twigs/Transaction.swift create mode 100644 Sources/twigs/TwigsApiService.swift create mode 100644 Sources/twigs/User.swift delete mode 100644 Sources/twigs/twigs.swift diff --git a/Package.swift b/Package.swift index 6c29201..4f69a4b 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,10 @@ import PackageDescription let package = Package( name: "twigs", + platforms: [ + .macOS(.v12), + .iOS(.v15) + ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/README.md b/README.md index 2dedf6e..a326bd8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# twigs +# Twigs -A description of this package. +Twigs is a personal finance/budgeting app aimed at individuals and families. This repository is the core code shared between the various platforms supported by Twigs. diff --git a/Sources/twigs/Budget.swift b/Sources/twigs/Budget.swift new file mode 100644 index 0000000..699d0ca --- /dev/null +++ b/Sources/twigs/Budget.swift @@ -0,0 +1,25 @@ +import Foundation + +struct Budget: Identifiable, Hashable, Codable { + let id: String + let name: String + let description: String? + let currencyCode: String? +} + +struct BudgetOverview { + let budget: Budget + let balance: Int + var expectedIncome: Int = 0 + var actualIncome: Int = 0 + var expectedExpenses: Int = 0 + var actualExpenses: Int = 0 +} + +protocol BudgetRepository { + func getBudgets(count: Int?, page: Int?) async throws -> [Budget] + func getBudget(_ id: String) async throws -> Budget + func newBudget(_ budget: Budget) async throws -> Budget + func updateBudget(_ budget: Budget) async throws -> Budget + func deleteBudget(_ id: String) async throws +} diff --git a/Sources/twigs/Category.swift b/Sources/twigs/Category.swift new file mode 100644 index 0000000..4f4e157 --- /dev/null +++ b/Sources/twigs/Category.swift @@ -0,0 +1,19 @@ +import Foundation + +struct Category: Identifiable, Hashable, Codable { + let budgetId: String + let id: String + let title: String + let description: String? + let amount: Int + let expense: Bool + let archived: Bool +} + +protocol CategoryRepository { + func getCategories(budgetId: String?, expense: Bool?, archived: Bool?, count: Int?, page: Int?) async throws -> [Category] + func getCategory(_ categoryId: String) async throws -> Category + func createCategory(_ category: Category) async throws -> Category + func updateCategory(_ category: Category) async throws -> Category + func deleteCategory(_ id: String) async throws +} diff --git a/Sources/twigs/RecurringTransaction.swift b/Sources/twigs/RecurringTransaction.swift new file mode 100644 index 0000000..eb5cda8 --- /dev/null +++ b/Sources/twigs/RecurringTransaction.swift @@ -0,0 +1,282 @@ +// +// File.swift +// +// +// Created by William Brawner on 12/22/21. +// + +import Foundation + +struct RecurringTransaction: Identifiable, Hashable, Codable { + let id: String + let title: String + let description: String? + let frequency: Frequency + let start: Date + let end: Date? + let amount: Int + let categoryId: String? + let expense: Bool + let createdBy: String + let budgetId: String +} + +struct Frequency: Hashable, Codable, CustomStringConvertible { + let unit: FrequencyUnit + let count: Int + let time: Time + + init?(unit: FrequencyUnit, count: Int, time: Time) { + if count < 1 { + return nil + } + self.unit = unit + self.count = count + self.time = time + } + + init?(from string: String) { + let parts = string.split(separator: ";") + guard let count = Int(parts[1]) else { + return nil + } + var timeIndex = 3 + switch parts[0] { + case "D": + self.unit = .daily + timeIndex = 2 + case "W": + let daysOfWeek = parts[2].split(separator: ",").compactMap { dayOfWeek in + DayOfWeek(rawValue: String(dayOfWeek)) + } + if daysOfWeek.isEmpty { + return nil + } + self.unit = .weekly(Set(daysOfWeek)) + case "M": + guard let dayOfMonth = DayOfMonth(from: String(parts[2])) else { + return nil + } + self.unit = .monthly(dayOfMonth) + case "Y": + guard let dayOfYear = DayOfYear(from: String(parts[2])) else { + return nil + } + self.unit = .yearly(dayOfYear) + default: + return nil + } + guard let time = Time(from: String(parts[timeIndex])) else { + return nil + } + self.time = time + self.count = count + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let frequencyString = try container.decode(String.self) + self.init(from: frequencyString)! + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } + + var description: String { + // TODO: Make the backend representation of this more sensible and then use this + // return [unit.description, count.description, time.description].joined(separator: ";") + let unitParts = "\(unit)".split(separator: ";") + if unitParts.count == 1 { + return [unitParts[0].description, count.description, time.description].joined(separator: ";") + } else{ + return [unitParts[0].description, count.description, unitParts[1].description, time.description].joined(separator: ";") + } + } + + var naturalDescription: String { + return unit.format(count: count, time: time) + } +} + +enum FrequencyUnit: Hashable, CustomStringConvertible { + case daily + case weekly(Set) + case monthly(DayOfMonth) + case yearly(DayOfYear) + + var description: String { + switch self { + case .daily: + return "D" + case .weekly(let daysOfWeek): + return String(format: "W;%s", daysOfWeek.map { $0.rawValue }.joined(separator: ",")) + case .monthly(let dayOfMonth): + return String(format: "M;%s", dayOfMonth.description) + case .yearly(let dayOfYear): + return String(format: "Y;%s", dayOfYear.description) + } + } + + func format(count: Int, time: Time) -> String { + switch self { + case .daily: + return String(localized: "Every \(count) day(s) at \(time.description)") + case .weekly(let daysOfWeek): + return String(localized: "Every \(count) week(s) on \(daysOfWeek.description) at \(time.description)") + case .monthly(let dayOfMonth): + return String(localized: "Every \(count) month(s) on the \(dayOfMonth.description) at \(time.description)") + case .yearly(let dayOfYear): + return String(localized: "Every \(count) year(s) on \(dayOfYear.description) at \(time.description)") + } + } +} + +struct Time: Hashable, CustomStringConvertible { + let hours: Int + let minutes: Int + let seconds: Int + + init?(hours: Int, minutes: Int, seconds: Int) { + if hours < 0 || hours > 23 { + return nil + } + if minutes < 0 || minutes > 59 { + return nil + } + if seconds < 0 || seconds > 59 { + return nil + } + self.hours = hours + self.minutes = minutes + self.seconds = seconds + } + + init?(from string: String) { + let parts = string.split(separator: ":").compactMap { + Int($0) + } + if parts.count != 3 { + return nil + } + self.init(hours: parts[0], minutes: parts[1], seconds: parts[2]) + } + + var description: String { + return String(format: "%02d:%02d:%02d", hours, minutes, seconds) + } +} + +enum DayOfMonth: Hashable, CustomStringConvertible { + case positional(Position, DayOfWeek) + case fixed(Int) + init?(position: Position, dayOfWeek: DayOfWeek) { + if position == .day { + return nil + } + self = .positional(position, dayOfWeek) + } + + init?(day: Int) { + if day < 1 || day > 31 { + return nil + } + self = .fixed(day) + } + + init?(from string: String) { + let parts = string.split(separator: "-") + guard let position = Position.init(rawValue: String(parts[0])) else { + return nil + } + if position == .day { + guard let day = Int(parts[1]) else { + return nil + } + self = .fixed(day) + } else { + guard let dayOfWeek = DayOfWeek(rawValue: String(parts[1])) else { + return nil + } + self = .positional(position, dayOfWeek) + } + } + + var description: String { + switch self { + case .positional(let position, let dayOfWeek): + return "\(position)-\(dayOfWeek)" + case .fixed(let day): + return "\(Position.day)-\(day)" + } + } +} + +enum Position: String, Hashable { + case day = "DAY" + case first = "FIRST" + case second = "SECOND" + case third = "THIRD" + case fourth = "FOURTH" + case last = "LAST" +} + +enum DayOfWeek: String, Hashable { + case monday = "MONDAY" + case tuesday = "TUESDAY" + case wednesday = "WEDNESDAY" + case thursday = "THURSDAY" + case friday = "FRIDAY" + case saturday = "SATURDAY" + case sunday = "SUNDAY" +} + +struct DayOfYear: Hashable, CustomStringConvertible { + let month: Int + let day: Int + + init?(month: Int, day: Int) { + var maxDay: Int + switch month { + case 2: + maxDay = 29; + break; + case 4, 6, 9, 11: + maxDay = 30; + break; + default: + maxDay = 31; + } + if day < 1 || day > maxDay { + return nil + } + if month < 1 || month > 12 { + return nil + } + self.day = day + self.month = month + } + + init?(from string: String) { + let parts = string.split(separator: "-").compactMap { + Int($0) + } + if parts.count < 2 { + return nil + } + self.init(month: parts[0], day: parts[1]) + } + + var description: String { + return String(format: "%02d-%02d", self.month, self.day) + } +} + +protocol RecurringTransactionsRepository { + func getRecurringTransactions(budgetId: String) async throws -> [RecurringTransaction] + func getRecurringTransaction(_ id: String) async throws -> RecurringTransaction + func createRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction + func updateRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction + func deleteRecurringTransaction(_ id: String) async throws +} diff --git a/Sources/twigs/Transaction.swift b/Sources/twigs/Transaction.swift new file mode 100644 index 0000000..bfce89c --- /dev/null +++ b/Sources/twigs/Transaction.swift @@ -0,0 +1,54 @@ +// +// File.swift +// +// +// Created by William Brawner on 12/22/21. +// + +import Foundation + +struct Transaction: Identifiable, Hashable, Codable { + let id: String + let title: String + let description: String? + let date: Date + let amount: Int + let categoryId: String? + let expense: Bool + let createdBy: String + let budgetId: String +} + +struct BalanceResponse: Codable { + let balance: Int +} + +enum TransactionType: Int, CaseIterable, Identifiable, Hashable { + case expense + case income + + var id: TransactionType { self } +} + +extension Transaction { + var type: TransactionType { + if (self.expense) { + return .expense + } else { + return .income + } + } + + var amountString: String { + return String(Double(self.amount) / 100.0) + } +} + +protocol TransactionRepository { + func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, to: Date?, count: Int?, page: Int?) async throws -> [Transaction] + func getTransaction(_ transactionId: String) async throws -> Transaction + func createTransaction(_ transaction: Transaction) async throws -> Transaction + func updateTransaction(_ transaction: Transaction) async throws -> Transaction + func deleteTransaction(_ transactionId: String) async throws + func sumTransactions(budgetId: String?, categoryId: String?, from: Date?, to: Date?) async throws -> BalanceResponse +} diff --git a/Sources/twigs/TwigsApiService.swift b/Sources/twigs/TwigsApiService.swift new file mode 100644 index 0000000..054e3d9 --- /dev/null +++ b/Sources/twigs/TwigsApiService.swift @@ -0,0 +1,442 @@ +// +// File.swift +// +// +// Created by William Brawner on 12/22/21. +// + +import Foundation + +class TwigsApiService: BudgetRepository, CategoryRepository, RecurringTransactionsRepository, TransactionRepository, UserRepository { + let requestHelper: RequestHelper + + convenience init(_ serverUrl: String) { + self.init(RequestHelper(serverUrl)) + } + + init(_ requestHelper: RequestHelper) { + self.requestHelper = requestHelper + } + + var token: String? { + get { + return requestHelper.token + } + set { + requestHelper.token = newValue + } + } + + // MARK: Budgets + func getBudgets(count: Int? = nil, page: Int? = nil) async throws -> [Budget] { + var queries = [String: Array]() + if count != nil { + queries["count"] = [String(count!)] + } + if (page != nil) { + queries["page"] = [String(page!)] + } + return try await requestHelper.get("/api/budgets", queries: queries) + } + + func getBudget(_ id: String) async throws -> Budget { + return try await requestHelper.get("/api/budgets/\(id)") + } + + func newBudget(_ budget: Budget) async throws -> Budget { + return try await requestHelper.post("/api/budgets", data: budget, type: Budget.self) + } + + func updateBudget(_ budget: Budget) async throws -> Budget { + return try await requestHelper.put("/api/budgets/\(budget.id)", data: budget) + } + + func deleteBudget(_ id: String) async throws { + return try await requestHelper.delete("/api/budgets/\(id)") + } + + // MARK: Transactions + + func getTransactions( + budgetIds: [String], + categoryIds: [String]? = nil, + from: Date? = nil, + to: Date? = nil, + count: Int? = nil, + page: Int? = nil + ) async throws -> [Transaction] { + var queries = [String: Array]() + queries["budgetIds"] = budgetIds + if categoryIds != nil { + queries["categoryIds"] = categoryIds! + } + if from != nil { + queries["from"] = [from!.toISO8601String()] + } + if to != nil { + queries["to"] = [to!.toISO8601String()] + } + if count != nil { + queries["count"] = [String(count!)] + } + if (page != nil) { + queries["page"] = [String(page!)] + } + return try await requestHelper.get("/api/transactions", queries: queries) + } + + func getTransaction(_ id: String) async throws -> Transaction { + return try await requestHelper.get("/api/transactions/\(id)") + } + + func createTransaction(_ transaction: Transaction) async throws -> Transaction { + return try await requestHelper.post("/api/transactions", data: transaction, type: Transaction.self) + } + + func updateTransaction(_ transaction: Transaction) async throws -> Transaction { + return try await requestHelper.put("/api/transactions/\(transaction.id)", data: transaction) + } + + func deleteTransaction(_ id: String) async throws { + return try await requestHelper.delete("/api/transactions/\(id)") + } + + func sumTransactions(budgetId: String? = nil, categoryId: String? = nil, from: Date? = nil, to: Date? = nil) async throws -> BalanceResponse { + var queries = [String: Array]() + if let budgetId = budgetId { + queries["budgetId"] = [budgetId] + } + if let categoryId = categoryId { + queries["categoryId"] = [categoryId] + } + if let from = from { + queries["from"] = [from.toISO8601String()] + } + if let to = to { + queries["to"] = [to.toISO8601String()] + } + return try await requestHelper.get("/api/transactions/sum", queries: queries) + } + + // MARK: Categories + + func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = nil, count: Int? = nil, page: Int? = nil) async throws -> [Category] { + var queries = [String: Array]() + if budgetId != nil { + queries["budgetIds"] = [String(budgetId!)] + } + if expense != nil { + queries["expense"] = [String(expense!)] + } + if archived != nil { + queries["archived"] = [String(archived!)] + } + if count != nil { + queries["count"] = [String(count!)] + } + if (page != nil) { + queries["page"] = [String(page!)] + } + return try await requestHelper.get("/api/categories", queries: queries) + } + + func getCategory(_ id: String) async throws -> Category { + return try await requestHelper.get("/api/categories/\(id)") + } + + func getCategoryBalance(_ id: String) async throws -> Int { + return try await requestHelper.get("/api/categories/\(id)/balance") + } + + func createCategory(_ category: Category) async throws -> Category { + return try await requestHelper.post("/api/categories", data: category, type: Category.self) + } + + func updateCategory(_ category: Category) async throws -> Category { + return try await requestHelper.put("/api/categories/\(category.id)", data: category) + } + + func deleteCategory(_ id: String) async throws { + return try await requestHelper.delete("/api/categories/\(id)") + } + + // MARK: Users + func login(username: String, password: String) async throws -> LoginResponse { + let response = try await requestHelper.post( + "/api/users/login", + data: LoginRequest(username: username, password: password), + type: LoginResponse.self + ) + self.requestHelper.token = response.token + return response + } + + func register(username: String, email: String, password: String) async throws -> User { + return try await requestHelper.post( + "/api/users/register", + data: RegistrationRequest(username: username, email: email, password: password), + type: User.self + ) + } + + func getUser(_ id: String) async throws -> User { + return try await requestHelper.get("/api/users/\(id)") + } + + func searchUsers(_ query: String) async throws -> [User] { + return try await requestHelper.get( + "/api/users/search", + queries: ["query": [query]] + ) + } + + func getUsers(count: Int? = nil, page: Int? = nil) async throws -> [User] { + var queries = [String: Array]() + if count != nil { + queries["count"] = [String(count!)] + } + if (page != nil) { + queries["page"] = [String(page!)] + } + return try await requestHelper.get("/api/Users", queries: queries) + } + + func newUser(_ user: User) async throws -> User { + return try await requestHelper.post("/api/users", data: user, type: User.self) + } + + func updateUser(_ user: User) async throws -> User { + return try await requestHelper.put("/api/users/\(user.id)", data: user) + } + + func deleteUser(_ user: User) async throws { + return try await requestHelper.delete("/api/users/\(user.id)") + } + + // MARK: Recurring Transactions + func getRecurringTransactions(budgetId: String) async throws -> [RecurringTransaction] { + return try await requestHelper.get("/api/recurringtransactions", queries: ["budgetId": [budgetId]]) + } + + func getRecurringTransaction(_ id: String) async throws -> RecurringTransaction { + return try await requestHelper.get("/api/recurringtransactions/\(id)") + } + + func createRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction { + return try await requestHelper.post("/api/recurringtransactions", data: transaction, type: RecurringTransaction.self) + } + + func updateRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction { + return try await requestHelper.put("/api/recurringtransactions/\(transaction.id)", data: transaction) + } + + func deleteRecurringTransaction(_ id: String) async throws { + return try await requestHelper.delete("/api/recurringtransactions/\(id)") + } +} + +class RequestHelper { + let decoder = JSONDecoder() + private var _baseUrl: String = "" + var baseUrl: String { + get { + self.baseUrl + } + set { + var correctServer = newValue.lowercased() + if !correctServer.starts(with: "http://") && !correctServer.starts(with: "https://") { + correctServer = "http://\(correctServer)" + } + self._baseUrl = correctServer + } + } + var token: String? + + init(_ serverUrl: String) { + self.baseUrl = serverUrl + self.decoder.dateDecodingStrategy = .formatted(Date.iso8601DateFormatter) + } + + func get( + _ endPoint: String, + queries: [String: Array]? = nil + ) async throws -> ResultType { + var combinedEndPoint = endPoint + if (queries != nil) { + for (key, values) in queries! { + for value in values { + let separator = combinedEndPoint.contains("?") ? "&" : "?" + combinedEndPoint += separator + key + "=" + value + } + } + } + + return try await buildRequest(endPoint: combinedEndPoint, method: "GET") + } + + func post( + _ endPoint: String, + data: Codable, + type: ResultType.Type + ) async throws -> ResultType { + return try await buildRequest( + endPoint: endPoint, + method: "POST", + data: data + ) + } + + func put( + _ endPoint: String, + data: ResultType + ) async throws -> ResultType { + return try await buildRequest( + endPoint: endPoint, + method: "PUT", + data: data + ) + } + + func delete(_ endPoint: String) async throws { + // Delete requests return no body so they need a special request helper + guard let url = URL(string: self.baseUrl + endPoint) else { + throw NetworkError.invalidUrl + } + + var request = URLRequest(url: url) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "DELETE" + + let (_, res) = try await URLSession.shared.data(for: request) + guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else { + switch (res as? HTTPURLResponse)?.statusCode { + case 400: throw NetworkError.badRequest + case 401, 403: throw NetworkError.unauthorized + case 404: throw NetworkError.notFound + default: throw NetworkError.unknown + } + } + } + + private func buildRequest( + endPoint: String, + method: String, + data: Encodable? = nil + ) async throws -> ResultType { + guard let url = URL(string: self.baseUrl + endPoint) else { + print("Unable to build url from base: \(self.baseUrl)") + throw NetworkError.invalidUrl + } + + print("\(method) - \(url)") + + var request = URLRequest(url: url) + request.httpBody = data?.toJSONData() + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = method + if let token = self.token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + let (data, res) = try await URLSession.shared.data(for: request) + guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else { + switch (res as? HTTPURLResponse)?.statusCode { + case 400: throw NetworkError.badRequest + case 401, 403: throw NetworkError.unauthorized + case 404: throw NetworkError.notFound + default: throw NetworkError.unknown + } + } + return try self.decoder.decode(ResultType.self, from: data) + } +} + +enum NetworkError: Error, Equatable { + static func == (lhs: NetworkError, rhs: NetworkError) -> Bool { + switch (lhs, rhs) { + case (.unknown, .unknown): + return true + case (.notFound, .notFound): + return true + case (.unauthorized, .unauthorized): + return true + case (.badRequest, .badRequest): + return true + case (.invalidUrl, .invalidUrl): + return true + case (let .jsonParsingFailed(error1), let .jsonParsingFailed(error2)): + return error1.localizedDescription == error2.localizedDescription + default: + return false + } + } + + var name: String { + get { + switch self { + case .unknown: + return "unknown" + case .notFound: + return "notFound" + case .deleted: + return "deleted" + case .unauthorized: + return "unauthorized" + case .badRequest: + return "badRequest" + case .invalidUrl: + return "invalidUrl" + case .jsonParsingFailed(_): + return "jsonParsingFailed" + } + } + } + + case unknown + case notFound + case deleted + case unauthorized + case badRequest + case invalidUrl + case jsonParsingFailed(Error) +} + +extension Encodable { + func toJSONData() -> Data? { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return try? encoder.encode(self) + } +} + +extension Date { + static let iso8601DateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.timeZone = TimeZone(identifier: "UTC") + return dateFormatter + }() + + static let localeDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyyyMMdd", options: 0, locale: Locale.current) + return dateFormatter + }() + + static var firstOfMonth: Date { + get { + return Calendar.current.dateComponents([.calendar, .year,.month], from: Date()).date! + } + } + + func toISO8601String() -> String { + return Date.iso8601DateFormatter.string(from: self) + } + + func toLocaleString() -> String { + return Date.localeDateFormatter.string(from: self) + } +} + diff --git a/Sources/twigs/User.swift b/Sources/twigs/User.swift new file mode 100644 index 0000000..c6e3291 --- /dev/null +++ b/Sources/twigs/User.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by William Brawner on 12/22/21. +// + +import Foundation + +struct User: Codable, Equatable, Hashable { + let id: String + let username: String + let email: String? + let avatar: String? +} + +struct LoginRequest: Codable { + let username: String + let password: String +} + +struct LoginResponse: Codable { + let token: String + let expiration: String + let userId: String +} + +struct RegistrationRequest: Codable { + let username: String + let email: String + let password: String +} + +protocol UserRepository { + func getUser(_ id: String) async throws -> User + func searchUsers(_ withUsername: String) async throws -> [User] + func login(username: String, password: String) async throws -> LoginResponse + func register(username: String, email: String, password: String) async throws -> User +} diff --git a/Sources/twigs/twigs.swift b/Sources/twigs/twigs.swift deleted file mode 100644 index 451c552..0000000 --- a/Sources/twigs/twigs.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct twigs { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Tests/twigsTests/twigsTests.swift b/Tests/twigsTests/twigsTests.swift index 117bd4a..ebf13b4 100644 --- a/Tests/twigsTests/twigsTests.swift +++ b/Tests/twigsTests/twigsTests.swift @@ -6,6 +6,6 @@ final class twigsTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(twigs().text, "Hello, World!") +// XCTAssertEqual(twigs().text, "Hello, World!") } }