twigs-swift-sdk/Sources/TwigsCore/TwigsApiService.swift

479 lines
16 KiB
Swift

//
// File.swift
//
//
// Created by William Brawner on 12/22/21.
//
import Foundation
open class TwigsApiService: BudgetRepository, CategoryRepository, RecurringTransactionsRepository, TransactionRepository, UserRepository {
let requestHelper: RequestHelper
public convenience init() {
self.init(RequestHelper())
}
public init(_ requestHelper: RequestHelper) {
self.requestHelper = requestHelper
}
public var baseUrl: String? {
get {
return requestHelper.baseUrl
}
set {
requestHelper.baseUrl = newValue
}
}
public var token: String? {
get {
return requestHelper.token
}
set {
requestHelper.token = newValue
}
}
// MARK: Budgets
open func getBudgets(count: Int? = nil, page: Int? = nil) async throws -> [Budget] {
var queries = [String: Array<String>]()
if count != nil {
queries["count"] = [String(count!)]
}
if (page != nil) {
queries["page"] = [String(page!)]
}
return try await requestHelper.get("/api/budgets", queries: queries)
}
open func getBudget(_ id: String) async throws -> Budget {
return try await requestHelper.get("/api/budgets/\(id)")
}
open func newBudget(_ budget: Budget) async throws -> Budget {
return try await requestHelper.post("/api/budgets", data: budget, type: Budget.self)
}
open func updateBudget(_ budget: Budget) async throws -> Budget {
return try await requestHelper.put("/api/budgets/\(budget.id)", data: budget)
}
open func deleteBudget(_ id: String) async throws {
return try await requestHelper.delete("/api/budgets/\(id)")
}
// MARK: Transactions
open 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<String>]()
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)
}
open func getTransaction(_ id: String) async throws -> Transaction {
return try await requestHelper.get("/api/transactions/\(id)")
}
open func createTransaction(_ transaction: Transaction) async throws -> Transaction {
return try await requestHelper.post("/api/transactions", data: transaction, type: Transaction.self)
}
open func updateTransaction(_ transaction: Transaction) async throws -> Transaction {
return try await requestHelper.put("/api/transactions/\(transaction.id)", data: transaction)
}
open func deleteTransaction(_ id: String) async throws {
return try await requestHelper.delete("/api/transactions/\(id)")
}
open func sumTransactions(budgetId: String? = nil, categoryId: String? = nil, from: Date? = nil, to: Date? = nil) async throws -> BalanceResponse {
var queries = [String: Array<String>]()
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
open func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = nil, count: Int? = nil, page: Int? = nil) async throws -> [Category] {
var queries = [String: Array<String>]()
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)
}
open func getCategory(_ id: String) async throws -> Category {
return try await requestHelper.get("/api/categories/\(id)")
}
open func getCategoryBalance(_ id: String) async throws -> Int {
return try await requestHelper.get("/api/categories/\(id)/balance")
}
open func createCategory(_ category: Category) async throws -> Category {
return try await requestHelper.post("/api/categories", data: category, type: Category.self)
}
open func updateCategory(_ category: Category) async throws -> Category {
return try await requestHelper.put("/api/categories/\(category.id)", data: category)
}
open func deleteCategory(_ id: String) async throws {
return try await requestHelper.delete("/api/categories/\(id)")
}
// MARK: Users
open 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
}
open 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
)
}
open func getUser(_ id: String) async throws -> User {
return try await requestHelper.get("/api/users/\(id)")
}
open func searchUsers(_ query: String) async throws -> [User] {
return try await requestHelper.get(
"/api/users/search",
queries: ["query": [query]]
)
}
open func getUsers(count: Int? = nil, page: Int? = nil) async throws -> [User] {
var queries = [String: Array<String>]()
if count != nil {
queries["count"] = [String(count!)]
}
if (page != nil) {
queries["page"] = [String(page!)]
}
return try await requestHelper.get("/api/Users", queries: queries)
}
open func newUser(_ user: User) async throws -> User {
return try await requestHelper.post("/api/users", data: user, type: User.self)
}
open func updateUser(_ user: User) async throws -> User {
return try await requestHelper.put("/api/users/\(user.id)", data: user)
}
open func deleteUser(_ user: User) async throws {
return try await requestHelper.delete("/api/users/\(user.id)")
}
// MARK: Recurring Transactions
open func getRecurringTransactions(_ budgetId: String) async throws -> [RecurringTransaction] {
return try await requestHelper.get("/api/recurringtransactions", queries: ["budgetId": [budgetId]])
}
open func getRecurringTransaction(_ id: String) async throws -> RecurringTransaction {
return try await requestHelper.get("/api/recurringtransactions/\(id)")
}
open func createRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction {
return try await requestHelper.post("/api/recurringtransactions", data: transaction, type: RecurringTransaction.self)
}
open func updateRecurringTransaction(_ transaction: RecurringTransaction) async throws -> RecurringTransaction {
return try await requestHelper.put("/api/recurringtransactions/\(transaction.id)", data: transaction)
}
open func deleteRecurringTransaction(_ id: String) async throws {
return try await requestHelper.delete("/api/recurringtransactions/\(id)")
}
}
public class RequestHelper {
let decoder = JSONDecoder()
private var _baseUrl: String? = nil
var baseUrl: String? {
get {
self._baseUrl
}
set {
guard var correctServer = newValue?.lowercased() else {
return
}
if !correctServer.starts(with: "http://") && !correctServer.starts(with: "https://") {
correctServer = "https://\(correctServer)"
}
self._baseUrl = correctServer
}
}
var token: String?
public init() {
self.decoder.dateDecodingStrategy = .formatted(Date.iso8601DateFormatter)
}
func get<ResultType: Codable>(
_ endPoint: String,
queries: [String: Array<String>]? = 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<ResultType: Codable>(
_ endPoint: String,
data: Codable,
type: ResultType.Type
) async throws -> ResultType {
return try await buildRequest(
endPoint: endPoint,
method: "POST",
data: data
)
}
func put<ResultType: Codable>(
_ 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 baseUrl = self.baseUrl else {
throw NetworkError.invalidUrl
}
guard let url = URL(string: baseUrl + endPoint) else {
throw NetworkError.invalidUrl
}
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "DELETE"
if let token = self.token {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
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(nil)
case 401, 403: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
default: throw NetworkError.unknown
}
}
}
private func buildRequest<ResultType: Codable>(
endPoint: String,
method: String,
data: Encodable? = nil
) async throws -> ResultType {
guard let baseUrl = self.baseUrl else {
throw NetworkError.invalidUrl
}
guard let url = URL(string: baseUrl + endPoint) else {
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")
}
var data: Data? = nil
var res: URLResponse?
do {
(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 ?? -1 {
case 400:
var reason: String? = nil
if let data = data {
reason = String(decoding: data, as: UTF8.self)
}
throw NetworkError.badRequest(reason)
case 401, 403: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
case 500...599: throw NetworkError.server
default: throw NetworkError.unknown
}
}
} catch {
switch (res as? HTTPURLResponse)?.statusCode ?? -1 {
case 401, 403: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
case 500...599: throw NetworkError.server
default:
var reason: String = error.localizedDescription
if let data = data {
reason = String(decoding: data, as: UTF8.self)
}
throw NetworkError.badRequest(reason)
}
}
do {
guard let data = data else {
throw NetworkError.unknown
}
return try self.decoder.decode(ResultType.self, from: data)
} catch {
print("error decoding json: \(error)")
if let data = data {
print(String(decoding: data, as: UTF8.self))
}
throw NetworkError.jsonParsingFailed(error)
}
}
}
public enum NetworkError: Error, Equatable {
public 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 (let .badRequest(reason1), let .badRequest(reason2)):
return reason1 == reason2
case (.invalidUrl, .invalidUrl):
return true
case (.server, .server):
return true
case (let .jsonParsingFailed(error1), let .jsonParsingFailed(error2)):
return error1.localizedDescription == error2.localizedDescription
default:
return false
}
}
public 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 .server:
return "server"
case .jsonParsingFailed(_):
return "jsonParsingFailed"
}
}
}
case unknown
case notFound
case deleted
case unauthorized
case badRequest(String?)
case invalidUrl
case server
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
}()
func toISO8601String() -> String {
return Date.iso8601DateFormatter.string(from: self)
}
}