New Recipe Edit View
This commit is contained in:
@@ -14,9 +14,7 @@ struct ApiRequest {
|
||||
let authString: String?
|
||||
let headerFields: [HeaderField]
|
||||
let body: Data?
|
||||
|
||||
/// The path to the Cookbook application on the nextcloud server.
|
||||
|
||||
|
||||
init(
|
||||
path: String,
|
||||
method: RequestMethod,
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//
|
||||
// CustomError.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public enum NotImplementedError: Error, CustomStringConvertible {
|
||||
case notImplemented
|
||||
public var description: String {
|
||||
return "Function not implemented."
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkError: String, Error {
|
||||
case missingUrl = "Missing URL."
|
||||
case parametersNil = "Parameters are nil."
|
||||
case encodingFailed = "Parameter encoding failed."
|
||||
case decodingFailed = "Data decoding failed."
|
||||
case redirectionError = "Redirection error"
|
||||
case clientError = "Client error"
|
||||
case serverError = "Server error"
|
||||
case invalidRequest = "Invalid request"
|
||||
case unknownError = "Unknown error"
|
||||
case dataError = "Invalid data error."
|
||||
}
|
||||
|
||||
public enum ServerError: Error {
|
||||
case unknownError, missingRequestBody, duplicateRecipe, noImage, missingRecipeName, recipeNotFound, deleteFailed, requestUnsuccessful
|
||||
|
||||
|
||||
static func decodeFromURLResponse(response: HTTPURLResponse?) -> ServerError? {
|
||||
guard let response = response else {
|
||||
return ServerError.unknownError
|
||||
}
|
||||
print("Status code: ", response.statusCode)
|
||||
switch response.statusCode {
|
||||
case 200...299: return nil
|
||||
case 400: return .missingRequestBody
|
||||
case 404: return .recipeNotFound
|
||||
case 409: return .duplicateRecipe
|
||||
case 406: return .noImage
|
||||
case 422: return .missingRecipeName
|
||||
case 500: return .requestUnsuccessful
|
||||
case 502: return .deleteFailed
|
||||
default: return ServerError.unknownError
|
||||
}
|
||||
}
|
||||
|
||||
var localizedDescription: LocalizedStringKey {
|
||||
switch self {
|
||||
case .noImage: return "The recipe has no image whose MIME type matches the Accept header"
|
||||
case .missingRecipeName: return "There was no name in the request given for the recipe. Cannot save the recipe."
|
||||
default: return "An unknown server error occured."
|
||||
}
|
||||
}
|
||||
|
||||
var localizedTitle: LocalizedStringKey {
|
||||
switch self {
|
||||
case .missingRequestBody: return "Missing Request Body"
|
||||
case .duplicateRecipe: return "Duplicate Recipe"
|
||||
case .noImage: return "Image MIME Error"
|
||||
case .missingRecipeName: return "Missing Name"
|
||||
default: return "Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Nextcloud Cookbook iOS Client/Network/NetworkError.swift
Normal file
23
Nextcloud Cookbook iOS Client/Network/NetworkError.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// CustomError.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum NetworkError: String, Error {
|
||||
case missingUrl = "Missing URL."
|
||||
case parametersNil = "Parameters are nil."
|
||||
case encodingFailed = "Parameter encoding failed."
|
||||
case decodingFailed = "Data decoding failed."
|
||||
case redirectionError = "Redirection error"
|
||||
case clientError = "Client error"
|
||||
case serverError = "Server error"
|
||||
case invalidRequest = "Invalid request"
|
||||
case unknownError = "Unknown error"
|
||||
case dataError = "Invalid data error."
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// NetworkHandler.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
struct NetworkHandler {
|
||||
static func sendHTTPRequest(
|
||||
_ requestWrapper: RequestWrapper,
|
||||
hostPath: String,
|
||||
authString: String?
|
||||
) async throws -> (Data?, NetworkError?) {
|
||||
print("Sending \(requestWrapper.getMethod()) request (path: \(requestWrapper.getPath())) ...")
|
||||
|
||||
// Prepare URL
|
||||
let urlString = hostPath + requestWrapper.getPath()
|
||||
let urlStringSanitized = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
let url = URL(string: urlStringSanitized!)!
|
||||
|
||||
// Create URL request
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
// Set URL method
|
||||
request.httpMethod = requestWrapper.getMethod()
|
||||
|
||||
// Set authentication string, if needed
|
||||
if let authString = authString {
|
||||
request.setValue(
|
||||
"Basic \(authString)",
|
||||
forHTTPHeaderField: "Authorization"
|
||||
)
|
||||
}
|
||||
|
||||
// Set other header fields
|
||||
for headerField in requestWrapper.getHeaderFields() {
|
||||
request.setValue(
|
||||
headerField.getValue(),
|
||||
forHTTPHeaderField: headerField.getField()
|
||||
)
|
||||
}
|
||||
|
||||
// Set http body
|
||||
if let body = requestWrapper.getBody() {
|
||||
request.httpBody = body
|
||||
}
|
||||
|
||||
print("Request:\nMethod: \(request.httpMethod)\nPath: \(request.url?.absoluteString)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)")
|
||||
|
||||
// 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)
|
||||
print("Response: ", response)
|
||||
print("Data: ", data?.description, data, String(data: data ?? Data(), encoding: .utf8))
|
||||
return (data, nil)
|
||||
} catch {
|
||||
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
||||
}
|
||||
}
|
||||
|
||||
private static 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
//
|
||||
// NetworkRequests.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RequestMethod: String {
|
||||
case GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
|
||||
|
||||
|
||||
enum ContentType: String {
|
||||
case JSON = "application/json",
|
||||
IMAGE = "image/jpeg",
|
||||
FORM = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
struct HeaderField {
|
||||
private let _field: String
|
||||
private let _value: String
|
||||
|
||||
func getField() -> String {
|
||||
return _field
|
||||
}
|
||||
|
||||
func getValue() -> String {
|
||||
return _value
|
||||
}
|
||||
|
||||
static func accept(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "accept", _value: value.rawValue)
|
||||
}
|
||||
|
||||
static func ocsRequest(value: Bool) -> HeaderField {
|
||||
return HeaderField(_field: "OCS-APIRequest", _value: value ? "true" : "false")
|
||||
}
|
||||
|
||||
static func contentType(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "Content-Type", _value: value.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum RequestPath {
|
||||
case CATEGORIES,
|
||||
RECIPE_LIST(categoryName: String),
|
||||
RECIPE_DETAIL(recipeId: Int),
|
||||
NEW_RECIPE,
|
||||
IMAGE(recipeId: Int, thumb: Bool),
|
||||
CONFIG,
|
||||
KEYWORDS
|
||||
|
||||
case LOGINV2REQ,
|
||||
CUSTOM(path: String),
|
||||
NONE
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .CATEGORIES: return "categories"
|
||||
case .RECIPE_LIST(categoryName: let name): return "category/\(name)"
|
||||
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)"
|
||||
case .IMAGE(recipeId: let recipeId, thumb: let thumb): return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
|
||||
case .NEW_RECIPE: return "recipes"
|
||||
case .CONFIG: return "config"
|
||||
case .KEYWORDS: return "keywords"
|
||||
|
||||
case .LOGINV2REQ: return "/index.php/login/v2"
|
||||
case .CUSTOM(path: let path): return path
|
||||
case .NONE: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestWrapper {
|
||||
private let _method: RequestMethod
|
||||
private let _path: RequestPath
|
||||
private let _headerFields: [HeaderField]
|
||||
private let _body: Data?
|
||||
private let _authenticate: Bool = true
|
||||
|
||||
private init(
|
||||
method: RequestMethod,
|
||||
path: RequestPath,
|
||||
headerFields: [HeaderField] = [],
|
||||
body: Data? = nil,
|
||||
authenticate: Bool = true
|
||||
) {
|
||||
self._method = method
|
||||
self._path = path
|
||||
self._headerFields = headerFields
|
||||
self._body = body
|
||||
}
|
||||
|
||||
func getMethod() -> String {
|
||||
return self._method.rawValue
|
||||
}
|
||||
|
||||
func getPath() -> String {
|
||||
return self._path.stringValue
|
||||
}
|
||||
|
||||
func getHeaderFields() -> [HeaderField] {
|
||||
return self._headerFields
|
||||
}
|
||||
|
||||
func getBody() -> Data? {
|
||||
return _body
|
||||
}
|
||||
|
||||
func needsAuth() -> Bool {
|
||||
return _authenticate
|
||||
}
|
||||
}
|
||||
|
||||
extension RequestWrapper {
|
||||
static func customRequest(
|
||||
method: RequestMethod,
|
||||
path: RequestPath,
|
||||
headerFields: [HeaderField] = [],
|
||||
body: Data? = nil,
|
||||
authenticate: Bool = true
|
||||
) -> RequestWrapper {
|
||||
let request = RequestWrapper(
|
||||
method: method,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
body: body,
|
||||
authenticate: authenticate
|
||||
)
|
||||
return request
|
||||
}
|
||||
|
||||
static func jsonGetRequest(path: RequestPath) -> RequestWrapper {
|
||||
let headerFields = [
|
||||
HeaderField.ocsRequest(value: true),
|
||||
HeaderField.accept(value: .JSON)
|
||||
]
|
||||
let request = RequestWrapper(
|
||||
method: .GET,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
authenticate: true
|
||||
)
|
||||
return request
|
||||
}
|
||||
|
||||
static func imageRequest(path: RequestPath) -> RequestWrapper {
|
||||
let headerFields = [
|
||||
HeaderField.ocsRequest(value: true),
|
||||
HeaderField.accept(value: .IMAGE)
|
||||
]
|
||||
let request = RequestWrapper(
|
||||
method: .GET,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
authenticate: true
|
||||
)
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
Nextcloud Cookbook iOS Client/Network/NetworkUtils.swift
Normal file
50
Nextcloud Cookbook iOS Client/Network/NetworkUtils.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// NetworkRequests.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RequestMethod: String {
|
||||
case GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
|
||||
enum ContentType: String {
|
||||
case JSON = "application/json",
|
||||
IMAGE = "image/jpeg",
|
||||
FORM = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
struct HeaderField {
|
||||
private let _field: String
|
||||
private let _value: String
|
||||
|
||||
func getField() -> String {
|
||||
return _field
|
||||
}
|
||||
|
||||
func getValue() -> String {
|
||||
return _value
|
||||
}
|
||||
|
||||
static func accept(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "accept", _value: value.rawValue)
|
||||
}
|
||||
|
||||
static func ocsRequest(value: Bool) -> HeaderField {
|
||||
return HeaderField(_field: "OCS-APIRequest", _value: value ? "true" : "false")
|
||||
}
|
||||
|
||||
static func contentType(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "Content-Type", _value: value.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipeImportRequest: Codable {
|
||||
let url: String
|
||||
}
|
||||
Reference in New Issue
Block a user