Networking rework: simplified API calls.

This commit is contained in:
Vicnet
2023-11-16 13:13:06 +01:00
parent 17678dea5f
commit 3563c23e29
14 changed files with 731 additions and 97 deletions

View File

@@ -0,0 +1,100 @@
//
// ApiRequest.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 16.11.23.
//
import Foundation
import OSLog
struct ApiRequest {
/// The server address, e.g. https://example.com
let serverAddress: String
let path: String
let method: RequestMethod
let authString: String?
let headerFields: [HeaderField]
let body: Data?
/// The path to the Cookbook application on the nextcloud server.
let cookbookPath = "/index.php/apps/cookbook"
init(
serverAdress: String,
path: String,
method: RequestMethod,
authString: String? = nil,
headerFields: [HeaderField] = [],
body: Data? = nil
) {
self.serverAddress = serverAdress
self.method = method
self.path = path
self.headerFields = headerFields
self.authString = authString
self.body = body
}
func send() async -> (Data?, NetworkError?) {
Logger.network.debug("\(method.rawValue) \(path) sending ...")
// Prepare URL
let urlString = serverAddress + cookbookPath + path
Logger.network.debug("Full path: \(urlString)")
guard let urlStringSanitized = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return (nil, .unknownError) }
guard let url = URL(string: urlStringSanitized) else { return (nil, .unknownError) }
// Create URL request
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
// Set authentication string, if needed
if let authString = authString {
request.setValue(
"Basic \(authString)",
forHTTPHeaderField: "Authorization"
)
}
// Set other header fields
for headerField in headerFields {
request.setValue(
headerField.getValue(),
forHTTPHeaderField: headerField.getField()
)
}
// Set http body
if let body = body {
request.httpBody = body
}
// Wait for and return data and (decoded) response
var data: Data? = nil
var response: URLResponse? = nil
do {
(data, response) = try await URLSession.shared.data(for: request)
Logger.network.debug("\(method.rawValue) \(path) SUCCESS!")
return (data, nil)
} catch {
let error = decodeURLResponse(response: response as? HTTPURLResponse)
Logger.network.debug("\(method.rawValue) \(path) FAILURE: \(error.debugDescription)")
return (nil, error)
}
}
private func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? {
guard let response = response else {
return NetworkError.unknownError
}
switch response.statusCode {
case 200...299: return (nil)
case 300...399: return (NetworkError.redirectionError)
case 400...499: return (NetworkError.clientError)
case 500...599: return (NetworkError.serverError)
case 600: return (NetworkError.invalidRequest)
default: return (NetworkError.unknownError)
}
}
}

View File

@@ -0,0 +1,187 @@
//
// CookbookApi.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 13.11.23.
//
import Foundation
import OSLog
import UIKit
protocol CookbookApi {
/// Not implemented yet.
static func importRecipe(
from serverAdress: String,
auth: String,
data: Data
) async -> (NetworkError?)
/// Get either the full image or a thumbnail sized version.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The according recipe id.
/// - size: The size of the image.
/// - Returns: The image of the recipe with the specified id. A NetworkError if the request fails, otherwise nil.
static func getImage(
from serverAdress: String,
auth: String,
id: Int,
size: RecipeImage.RecipeImageSize
) async -> (UIImage?, NetworkError?)
/// Get all recipes.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of all recipes.
static func getRecipes(
from serverAdress: String,
auth: String
) async -> ([Recipe]?, NetworkError?)
/// Create a new recipe.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func createRecipe(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Get the recipe with the specified id.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: The recipe if it exists. A NetworkError if the request fails.
static func getRecipe(
from serverAdress: String,
auth: String, id: Int
) async -> (RecipeDetail?, NetworkError?)
/// Update an existing recipe with new entries.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func updateRecipe(
from serverAdress: String,
auth: String, id: Int
) async -> (NetworkError?)
/// Delete the recipe with the specified id.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func deleteRecipe(
from serverAdress: String,
auth: String,
id: Int
) async -> (NetworkError?)
/// Get all categories.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of categories. A NetworkError if the request fails.
static func getCategories(
from serverAdress: String,
auth: String
) async -> ([String]?, NetworkError?)
/// Get all recipes of a specified category.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - categoryName: The category name.
/// - Returns: A list of recipes. A NetworkError if the request fails.
static func getCategory(
from serverAdress: String,
auth: String,
named categoryName: String
) async -> ([Recipe]?, NetworkError?)
/// Rename an existing category.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - categoryName: The name of the category to be renamed.
/// - newName: The new category name.
/// - Returns: A NetworkError if the request fails.
static func renameCategory(
from serverAdress: String,
auth: String,
named categoryName: String,
newName: String
) async -> (NetworkError?)
/// Get all keywords/tags.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of tag strings. A NetworkError if the request fails.
static func getTags(
from serverAdress: String,
auth: String
) async -> ([String]?, NetworkError?)
/// Get all recipes tagged with the specified keyword.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - keyword: The keyword.
/// - Returns: A list of recipes tagged with the specified keyword. A NetworkError if the request fails.
static func getRecipesTagged(
from serverAdress: String,
auth: String,
keyword: String
) async -> ([Recipe]?, NetworkError?)
/// Get the servers api version.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A NetworkError if the request fails.
static func getApiVersion(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Trigger a reindexing action on the server.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func postReindex(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Get the current configuration of the Cookbook server application.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func getConfig(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Set the current configuration of the Cookbook server application.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func postConfig(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
}

View File

@@ -0,0 +1,207 @@
//
// CookbookApiV1.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 16.11.23.
//
import Foundation
import UIKit
class CookbookApiV1: CookbookApi {
static func importRecipe(from serverAdress: String, auth: String, data: Data) async -> (NetworkError?) {
return .unknownError
}
static func getImage(from serverAdress: String, auth: String, id: Int, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
let imageSize = (size == .FULL ? "full" : "thumb")
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)/image?size=\(imageSize)",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .IMAGE)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (UIImage(data: data), error)
}
static func getRecipes(from serverAdress: String, auth: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func createRecipe(from serverAdress: String, auth: String) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes",
method: .POST,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (error) }
do {
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
if let id = json as? Int {
return nil
} else if let dict = json as? [String: Any] {
return .serverError
}
} catch {
return .decodingFailed
}
return nil
}
static func getRecipe(from serverAdress: String, auth: String, id: Int) async -> (RecipeDetail?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func updateRecipe(from serverAdress: String, auth: String, id: Int) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
method: .PUT,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (error) }
do {
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
if let id = json as? Int {
return nil
} else if let dict = json as? [String: Any] {
return .serverError
}
} catch {
return .decodingFailed
}
return nil
}
static func deleteRecipe(from serverAdress: String, auth: String, id: Int) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
method: .DELETE,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (error) }
return nil
}
static func getCategories(from serverAdress: String, auth: String) async -> ([String]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/categories",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func getCategory(from serverAdress: String, auth: String, named categoryName: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/category/\(categoryName)",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func renameCategory(from serverAdress: String, auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/category/\(categoryName)",
method: .PUT,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (error) }
return nil
}
static func getTags(from serverAdress: String, auth: String) async -> ([String]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/keywords",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func getRecipesTagged(from serverAdress: String, auth: String, keyword: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/tags/\(keyword)",
method: .GET,
authString: auth,
headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)]
)
let (data, error) = await request.send()
guard let data = data else { return (nil, error) }
return (JSONDecoder.safeDecode(data), nil)
}
static func getApiVersion(from serverAdress: String, auth: String) async -> (NetworkError?) {
return .none
}
static func postReindex(from serverAdress: String, auth: String) async -> (NetworkError?) {
return .none
}
static func getConfig(from serverAdress: String, auth: String) async -> (NetworkError?) {
return .none
}
static func postConfig(from serverAdress: String, auth: String) async -> (NetworkError?) {
return .none
}
}