
Наверняка, каждый разработчик, которому необходимо было программировать сетевой слой приложения решал задачу передачи параметров запроса. В большинстве случаев это несложная задача, которая решается стандартными средствами, которые предоставляет нативный sdk либо язык программирования. Но если рассматривать ситуацию в контексте платформы iOS и языка программирования Swift, то тут же станет ясно, что компилятор выдает ошибку при попытке сериализации параметров в виде словаря [String: Any]. Однако, благодаря нововведениям, которые появились в iOS 15.4 и Swift 5.6 данный словарь наконец-то стало возможно сериализовать.
Задача
-
В случае передачи параметров в body запроса требуется возможность объявления в виде словаря [String: Any].
let requestParameters = [ "method": "createUser", "credentials": [ "login": login, "password": password, "email": email, "fullName": [ "firstName": firstName, "lastName": lastName ] ] ]
При таком подходе становится намного легче поддерживать проект, так как исключается создание «ненужных» моделей.
struct LoginRequest: Codable { struct Credentials: Codable { struct FullName: Codable { var firstName: String var lastName: String } var login: String var password: String var email: String var fullName: FullName } var method: String var credentials: Credentials }
Сразу сделаем оговорку, что передавать такие параметры как логин и пароль в сетевом запросе не стоит, поскольку для этих целей существует уже устаревшая технология Basic authentication, а также более современный подход с использованием access token и refresh token.
-
В случае передачи параметров в виде query строки требуется, чтобы порядок следования при инициализации
let requestParameters = [ "email": email, "firstName": firstName, "lastName": lastName ]
был таким же в самой строке
email=example@example.com&firstName=Nickey&lastName=Santoro
Это требование необходимо для случаев криптования параметров (когда дополнительно передается зашифрованный hash данной строки).
Решение
Приступим к сериализации параметров в body запроса. Благодаря протоколу CodingKeyRepresentable, появившемуся в iOS 15.4 и технике type erasure, появившейся в Swift 5.6 упростилось энкодирование Any типа.
Предположим, существует некоторый контейнер для параметров — Query, который в качестве хранилища использует массив для того, чтобы сохранить порядок следования элементов, но в то же время реализует все возможности словаря.
import Foundation public extension Request { struct Query<Key: Encodable> { // MARK: - Types public struct Parameter<K, V> { public var key: K public var value: V } // MARK: - Properties private var elements: [Element] // MARK: - Lifecycle init<S: Sequence>(uniqueKeysWithValues elements: S) where S.Element == (Key, Value) { self.elements = elements.map(Parameter.init) } // MARK: - Methods public subscript(key: Key) -> Value? where Key: Equatable { get { elements.first { $0.key == key }?.value } set { if let index = elements.firstIndex(where: { $0.key == key }) { if let newValue { elements[index].value = newValue } else { elements.remove(at: index) } } else { if let newValue { elements.append(Element(key: key, value: newValue)) } } } } } } // MARK: - Extensions extension Request.Query: ExpressibleByDictionaryLiteral { public typealias Value = any Encodable public init(dictionaryLiteral elements: (Key, Value)...) { self.elements = elements.map(Parameter.init) } } extension Request.Query: RangeReplaceableCollection { public init() { self.elements = [] } } extension Request.Query: Sequence { public typealias Iterator = IndexingIterator<Array<Element>> public func makeIterator() -> Iterator { return elements.makeIterator() } } extension Request.Query: Collection { public typealias Element = Parameter<Key, Value> public typealias Index = Int public var startIndex: Index { return elements.startIndex } public var endIndex: Int { return elements.endIndex } public subscript(position: Int) -> Element { return elements[position] } public func index(after i: Int) -> Int { return elements.index(after: i) } }
Тогда, для того, чтобы воспользоваться протоколом CodingKeyRepresentable нужен будет объект, реализующий CodingKey протокол.
import Foundation struct QueryCodingKey: CodingKey { let stringValue: String let intValue: Int? init(stringValue: String) { self.stringValue = stringValue self.intValue = Int(stringValue) } init(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } init(key: CodingKeyRepresentable) { self.stringValue = key.codingKey.stringValue self.intValue = key.codingKey.intValue } }
В результате, для реализации Encodable протокола, достаточно запрограммировать метод encode(to:).
extension Request.Query: Encodable { public func encode(to encoder: Encoder) throws { if Key.self is CodingKeyRepresentable.Type { var container = encoder.container(keyedBy: QueryCodingKey.self) for element in elements { guard let key = element.key as? CodingKeyRepresentable else { continue } let codingKey = QueryCodingKey(key: key) try container.encode(element.value, forKey: codingKey) } } else { var container = encoder.unkeyedContainer() for element in elements { try container.encode(element.key) try container.encode(element.value) } } } }
Далее решим задачу энкодирования в query строку. Во-первых существует несколько вариантов кодирования массива и bool значений, поэтому нужно эти варианты описать, например в виде соответствующих перечислений.
public struct QueryEncoding { public enum ArrayEncoding { case enclosingBrackets case surroundingBrackets case noBrackets } public enum BoolEncoding { case numeric case literal } public var array: ArrayEncoding public var bool: BoolEncoding public init(array: QueryEncoding.ArrayEncoding = .enclosingBrackets, bool: QueryEncoding.BoolEncoding = .literal) { self.array = array self.bool = bool } }
Далее, для того, чтобы создать query строку следует воспользоваться штатными средствами URLComponents и URLQueryItem. Таким образом, для преобразования каждого параметра в URLQueryItem достаточно объявить соответсвующий метод.
extension Request.Query where Key == String { public func encode(to url: URL, encoding: QueryEncoding) -> URL? { var components = URLComponents(url: url, resolvingAgainstBaseURL: false) components?.queryItems = elements.flatMap { encodeQueryItem(element: $0, encoding: encoding) } return components?.url } private func encodeQueryItem(element: Element, encoding: QueryEncoding) -> [URLQueryItem] { encodeQueryItem(name: element.key, value: element.value, encoding: encoding) } private func encodeQueryItem(name: String, value: Any, encoding: QueryEncoding) -> [URLQueryItem] { switch value { case let boolean as Bool: let queryItem = encodeBool(name: name, value: boolean, encoding: encoding) return [queryItem] case let number as NSNumber: let queryItem = encodeNumber(name: name, value: number) return [queryItem] case let array as [Any]: let queryItems = encodeArray(name: name, value: array, encoding: encoding) return queryItems case let dictionary as [String: Any]: let queryItems = encodeDictionary(name: name, value: dictionary, encoding: encoding) return queryItems default: let queryItem = URLQueryItem(name: name, value: "\(value)") return [queryItem] } } private func encodeBool(name: String, value: Bool, encoding: QueryEncoding) -> URLQueryItem { let stringValue: String switch encoding.bool { case .numeric: stringValue = (value as NSNumber).stringValue case .literal: stringValue = String(value) } return URLQueryItem(name: name, value: stringValue) } private func encodeNumber(name: String, value: NSNumber) -> URLQueryItem { let stringValue = value.stringValue return URLQueryItem(name: name, value: stringValue) } private func encodeArray(name: String, value: [Any], encoding: QueryEncoding) -> [URLQueryItem] { switch encoding.array { case .enclosingBrackets: return value.flatMap { encodeQueryItem(name: name + "[]", value: $0, encoding: encoding) } case .surroundingBrackets: let value = value .flatMap { encodeQueryItem(name: name, value: $0, encoding: encoding) } .compactMap { $0.value } .map { "\"\($0)\"" } .joined(separator: ",") let queryItem = URLQueryItem(name: name, value: "[\(value)]") return [queryItem] case .noBrackets: return value.flatMap { encodeQueryItem(name: name, value: $0, encoding: encoding) } } } private func encodeDictionary(name: String, value: [String: Any], encoding: QueryEncoding) -> [URLQueryItem] { return value .map { encodeQueryItem(name: name + "[\($0)]", value: $1, encoding: encoding) } .flatMap { $0 } } }
Внимательный читатель может спросить, для чего добавлен constraint?
where Key == String
Это ограничение обусловлено типом (String) первого поля структуры URLQueryItem.
Использование
В первом случае, когда требуется передать параметры в body запроса метод для создания и выполнения запроса будет следующим
func createAccount(login: String, password: String, email: String, firstName: String, lastName: String) async throws { let url = try createUrl(host: .staging, path: "api/v1/account/create") let headers: [HTTPHeader] = [ .contentType("application/json"), ] let parameters: Request.Query = [ "method": "createUser", "credentials": [ "login": login, "password": password, "email": email, "fullName": [ "firstName": firstName, "lastName": lastName ] ] ] try await dataRequest( url: url, method: .post, headers: headers, parameters: .body(parameters) ) }
Во втором случае, для создания query строки запрос представлен ниже
func updateAccount(id: Int, email: String, firstName: String, lastName: String) async throws { let url = try createUrl(host: .staging, path: "api/v1/account/\(id)") let accessToken = try accessToken(for: .staging) let headers: [HTTPHeader] = [ .authorization("Bearer \(accessToken)") ] let parameters: Request.Query = [ "email": email, "firstName": firstName, "lastName": lastName ] try await dataRequest( url: url, method: .put, headers: headers, parameters: .query(parameters) ) }
Заключение
Раньше, до появления CodingKeyRepresentable и type erasure, данное решение тоже можно было запрограммировать, только для этого нужно было дополнительно создавать контейнер AnyEncodable и проверять поле key на соответствие типу String или Int. Однако с развитием платформы намного удобней стало работать со словарем параметров и про очередную request модель наконец-то можно забыть.
ссылка на оригинал статьи https://habr.com/ru/post/678304/
Добавить комментарий