Как автоматизировать безопасный декодинг массивов в Swift с @propertyWrapper

от автора

Привет! На связи Влад, iOS-разработчик из Ozon. Сегодня я поделюсь с вами, возможно, не самым очевидным способом использования propertyWrappers. Обёртки позволяют добавлять дополнительную логику свойствам. В одну из них мы спрятали описание безопасного декодинга массивов, и теперь нам достаточно пометить свойство как @SafeDecode — и всё начинает работает автоматически. О том, как они работают и как их завести у себя, читайте дальше.

Что такое безопасный декодинг

Для тех, кто сталкивается с безопасным декодингом впервые, поясню: безопасный декодинг массива — это декодинг, при котором в декодируемом массиве может содержаться элемент, не соответствующий ожидаемому формату; при этом в результате мы получим все элементы массива, которые смогли распарсить.

Например, у нас есть структура:

struct Article {   let title: String // обязательное поле   let subtitle: String? // не обязательное поле }

И мы пытаемся распарсить такой массив данных:

[     {         "title": "Title1",         "subtitle": "Subtitle1"     },     {         // В этом элементе нет: "title": "Title1",         "subtitle": "Subtitle1"     } ]
do { let articles = try JSONDecoder().decode([Article].self, from: jsonData)  } catch {  print(error)   // Мы получим ошибку: "No value associated with key title (\"title\")."   // Потому что во втором элементе нет title, из-за этого    // весь массив не распарсится }

Чтобы всё-таки получить все остальные элементы, мы используем propertyWrapper. Он содержит внутри логику, которая фильтрует ошибки и возвращает полученные значения.

Для тех, кто ещё не работал с propertyWrapper

Если вы уже знаете, как работает обёртка свойств, смело переходите к следующему разделу. Или можете освежить знания.

PropertyWrapper — это обёртка, позволяющая добавлять дополнительную логику самому свойству. То есть, например, мы можем сделать так, чтобы все слова в строке начинались с заглавной буквы или чтобы числа в переменной были всегда меньше 12. И всё это — всего одной строкой. 

Давайте попробуем.

Для начала сделаем основу propertyWrapper:

@propertyWrapper struct Example {   public var wrappedValue: Any       public init(wrappedValue: Any) {       self.wrappedValue = wrappedValue    } }

Она состоит из маркировки @propertyWrapper и обязательного свойства wrappedValue. 

Эту обёртку уже можно использовать:

struct Numbers { @Example let value: Any }

Но она пока что ничего не делает.

Посмотрим, как выглядит propertyWrapper, который будет устанавливать в свойство только положительные числа с помощью abs():

@propertyWrapper struct Abs {     private var number: Int = 0     var wrappedValue: Int {         get { number }         set { number = abs(newValue) }     } }

Вся логика работы у нас спрятана в одной строке: number = abs(newValue). Чтобы сделать из этой обёртки что-то новое, достаточно поменять только эту строку.

Также у нас нет init(wrappedValue: Any), как в основе, потому что мы сразу задали значение для number. Если этого не сделать, придётся дописать init().

Пример использования:

struct Number {     @Abs var nonNegativeNumber: Int }  var number = Number() number.nonNegativeNumber = -1 print(number.nonNegativeNumber) // 1 number.nonNegativeNumber = -77 print(number.nonNegativeNumber) // 77

Теперь любое число, установленное в nonNegativeNumber, будет положительным благодаря обёртке @Abs.

Давайте посмотрим, как ещё можно сделать эту же обёртку. Мы можем вместо приватного number сделать всё в wrappedValue, для этого нам понадобится наблюдатель свойства didSet {}:

@propertyWrapper struct Abs {     var wrappedValue: Int {         didSet { wrappedValue = abs(wrappedValue) }     }      init(wrappedValue: Int) {         self.wrappedValue = abs(wrappedValue)     } }

Результат будет тот же:

struct Number {     @Abs var nonNegativeNumber: Int }  var number = Number() number.nonNegativeNumber = -15 print(number.nonNegativeNumber) // 15 number.nonNegativeNumber = -40 print(number.nonNegativeNumber) // 40

А теперь рассмотрим пример, в котором propertyWrapper будет удалять цифры из конца строки:

@propertyWrapper struct WithoutDecimalDigits {     var wrappedValue: String {         didSet { wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits) }     }      init(wrappedValue: String) {         self.wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits)     } }

Вся логика работы содержится в didSet{}. При таком подходе нам обязательно нужно установить значение wrappedValue через init(). Это связано с тем, что наблюдатели свойств начинают работать только после установки значения в объект. Проще говоря, блок didSet{} заработает только после установки значения wrappedValue в init().

Реализация:

struct Example {     @WithoutDecimalDigits var value: String }   let exampleString = Example(value: "Hello 123") print(exampleString.value) // "Hello "

Теперь наша обёртка удаляет все цифры из строки.

Зная эти основы, можно делать удобные propertyWrappers для своего проекта. Но использовать их нужно с осторожностью. Если скрыть внутри сложную логику, то, в будущем, можно случайно добавить неочевидное поведение.

Как безопасно декодировать массив с propertyWrapper

Обёртки очень легко использовать:

struct Example: Decodable { @SafeArray let articlesArray: [Article] }

Мы помечаем декодируемый массив как @SafeArray — и в нём будут все элементы, которые можно получить.

Чтобы propertyWrapper заработал, нужно сделать две вещи:

  1. Подготовить новый тип Throwable, который может содержать либо значение, либо ошибку.

  2. Написать расширение для SingleValueDecodingContainer.

Делаем тип, он будет очень простым:

enum Throwable<T: Decodable>: Decodable {     case success(T)     case failure(Error)      init(from decoder: Decoder) throws {         do {             let decoded = try T(from: decoder)             self = .success(decoded)         } catch let error {             self = .failure(error)         }     } }

А теперь сделаем расширение.

Шаг 1. Подготавливаем расширение:

extension SingleValueDecodingContainer {     func safelyDecodeArray<T>() throws -> [T] where T: Decodable {            } }

Шаг 2. Добавляем декодинг массива:

extension SingleValueDecodingContainer {     func safelyDecodeArray<T>() throws -> [T] where T: Decodable {       let decodedArray = (try? decode([Throwable<T>].self)) ?? []     } }

Шаг 3. Фильтруем и возвращаем декодируемый массив:

extension SingleValueDecodingContainer {     func safelyDecodeArray<T>() throws -> [T] where T: Decodable {         let decodedArray = (try? decode([Throwable<T>].self)) ?? []          let filtredArray = decodedArray.compactMap { result -> T? in             switch result {             case let .success(value):                 return value             case .failure(_):                 return nil             }         }          return filtredArray     } }

В результате декодинга safelyDecodeArray вернёт либо все полученные элементы, либо пустой массив.

Следующие два шага — для тех, кто хочет добавить обработку ошибок и проверку на пустой массив; если вы хотите сразу перейти к реализации propertyWrapper, их можно пропустить.

Шаг 4. Добавляем проверку и возвращаем ошибку, если после фильтрации получился пустой массив:

extension SingleValueDecodingContainer {     func safelyDecodeArray<T>() throws -> [T] where T: Decodable {        ...          if filtredArray.isEmpty {             throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed")         }          return filtredArray     } }

Шаг 5. Добавляем возможность выводить все полученные ошибки через callback:

extension SingleValueDecodingContainer {     // 1. Добавим callback для вывода описания ошибок onItemError: (([String: Any]) -> Void)?      func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?) throws -> [T] where T: Decodable {          let decodedArray = (try? decode([Throwable<T>].self)) ?? []          // 2. Чтобы иметь доступ к индексу элемента, добавим enumerated() и index         let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in             switch result {             case let .success(value):                 return value             // 3. Добавим errorInfo и его передачу через callback             case let .failure(error):                 var errorInfo = [String: Any]()                 errorInfo["error"] = error                 errorInfo["index"] = index                 onItemError?(errorInfo)                 return nil             }         }          if filtredArray.isEmpty {             throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed")         }          return filtredArray     } }

Теперь у нас есть возможность использовать вывод описания ошибок декодинга в нашем propertyWrapper.

Финальный шаг. Реализуем propertyWrapper:

@propertyWrapper public struct SafeArray<T: Decodable>: Decodable {     public let wrappedValue: [T]      public init(wrappedValue: [T]) {         self.wrappedValue = wrappedValue     }        public init(from decoder: Decoder) throws {         let container = try decoder.singleValueContainer()         wrappedValue = try container.safelyDecodeArray()     } }

Это всё, что нужно сделать, чтобы использовать обёртку для безопасного декодинга массивов. Теперь можно помечать массивы как SafeArray — и всё заработает автоматически.

Код из статьи целиком вы найдёте в последнем разделе.

Дополнительные propertyWrappers

В примере мы парсили массив в константу. Если нам нужно менять массив после парсинга, достаточно заменить в обёртке let на var, потому что обёрнутое свойство должно быть таким же, как wrappedValue:

@propertyWrapper public struct SafeMutableArray<T: Decodable>: Decodable {     public var wrappedValue: [T]   ... }

Тогда свойство тоже можно будет сделать переменной:

struct Example: Decodable {     @SafeMutableArray var articlesArray: [Article] }

Если нам нужно получить опциональный массив, необходимо добавить опциональность и для wrappedValue:

@propertyWrapper public struct SafeOptionalArray<T: Decodable>: Decodable {     public let wrappedValue: [T]?      public init(wrappedValue: [T]?) {         self.wrappedValue = wrappedValue     }      public init(from decoder: Decoder) throws {         let container = try decoder.singleValueContainer()         wrappedValue = try? container.safelyDecodeArray()     } }

Чтобы после декодинга опциональный массив можно было изменить, достаточно снова заменить let wrappedValue на var wrappedValue.

Вместо вывода

Это был, на мой взгляд, не самый очевидный способ декодирования данных в Swift, однако это ещё не все возможности property Wrapper. 

Так как обёртки используются в структурах и классах, то их можно попробовать использовать в любом месте приложения, добавляя любую нужную логику, которую можно уместить. 

Но всегда помните о том, что большая сила влечёт за собой и большую ответственность: если оставить внутри обёртки сложную логику, то она может аукнуться неочевидным поведением обёрнутого свойства. Применяйте инструмент там, где это действительно необходимо и к месту.

Код из статьи
@propertyWrapper public struct SafeArray<T: Decodable>: Decodable {     public let wrappedValue: [T]     public init(wrappedValue: [T]) {         self.wrappedValue = wrappedValue     }          public init(from decoder: Decoder) throws {         let container = try decoder.singleValueContainer()         wrappedValue = try container.safelyDecodeArray(onItemError: nil)     } }  extension SingleValueDecodingContainer {     func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?)     throws -> [T] where T: Decodable {         let decodedArray = (try? decode([Throwable<T>].self)) ?? []         let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in             switch result {             case let .success(value):                 return value             case let .failure(error):                 var errorInfo = [String: Any]()                 errorInfo["error"] = error                 errorInfo["index"] = index                 onItemError?(errorInfo)                 return nil             }         }                  if filtredArray.isEmpty {             throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed")         }                  return filtredArray     } }  enum Throwable<T: Decodable>: Decodable {     case success(T)     case failure(Error)          init(from decoder: Decoder) throws {         do {             let decoded = try T(from: decoder)             self = .success(decoded)         } catch let error {             self = .failure(error)         }     } }


ссылка на оригинал статьи https://habr.com/ru/articles/571006/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *