Можно ли создать пользовательский Swift KeyEncodingStrategy только для перечислений? - PullRequest
0 голосов
/ 19 июня 2019

Обновление 20 июня 2019: Благодаря @rudedog я пришел к рабочему решению.Я добавил реализацию ниже моего исходного сообщения ...


Обратите внимание, что я НЕ ищу "использовать private enum CodingKeys: String, CodingKey" в вашем объявлении struct / enum.

У меня есть ситуация, в которой для вызова службы требуется верхний регистр snake_case (UPPER_SNAKE_CASE) для всех перечислений.

Учитывая следующее struct:

public struct Request: Encodable {
    public let foo: Bool?
    public let barId: BarIdType

    public enum BarIdType: String, Encodable {
        case test
        case testGroup
    }
}

Все перечисления в любом запросеследует преобразовать в UPPER_SNAKE_CASE.

Например, при отправке let request = Request(foo: true, barId: testGroup) должен выглядеть следующим образом:

{
    "foo": true,
    "barId": "TEST_GROUP"
}

Я хотел бы предоставить пользовательский JSONEncoder.KeyEncodingStrategy, который будет ТОЛЬКОприменять к enum типам.

Создание собственной стратегии кажется простым, по крайней мере, согласно документации Apple JSONEncoder.KeyEncodingStrategy.custom (_:) .

Вот чтоУ меня так далеко:

public struct AnyCodingKey : CodingKey {

    public var stringValue: String
    public var intValue: Int?

    public init(_ base: CodingKey) {
        self.init(stringValue: base.stringValue, intValue: base.intValue)
    }

    public init(stringValue: String) {
        self.stringValue = stringValue
    }

    public init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }

    public init(stringValue: String, intValue: Int?) {
        self.stringValue = stringValue
        self.intValue = intValue
    }
}

extension JSONEncoder.KeyEncodingStrategy {

    static var convertToUpperSnakeCase: JSONEncoder.KeyEncodingStrategy {
        return .custom { keys in // codingKeys is [CodingKey]
            // keys = Enum ???

            var key = AnyCodingKey(keys.last!)
            // key = Enum ???

            key.stringValue = key.stringValue.toUpperSnakeCase // toUpperSnakeCase is a String extension
            return key
        }
    }
}

Я застрял, пытаясь определить, представляет ли [CodingKey] перечисление, или представляет ли отдельный CodingKey перечисление, и должно ли это стать UPPER_SNAKE_CASE.

Я знаю, это звучит бессмысленно, поскольку я могу просто предоставить жестко закодированные CodingKeys, но у нас много сервисных вызовов, требующих одинаковой обработки перечислений.Было бы проще просто указать пользовательский KeyEncodingStrategy для кодировщика.

Что было бы идеально, это применить JSONEncoder.KeyEncodingStrategy.convertToSnakeCase в пользовательской стратегии, а затем просто вернуть значение в верхнем регистре.Но опять же, только если значение представляет регистр перечисления.

Есть мысли?


Вот код, к которому я пришел, который решил мою проблему с помощью @rudedog:

import Foundation

public protocol UpperSnakeCaseRepresentable: Encodable {
    var upperSnakeCaseValue: String { get }
}

extension UpperSnakeCaseRepresentable where Self: RawRepresentable, Self.RawValue == String {
    var upperSnakeCaseValue: String {
        return _upperSnakeCaseValue(rawValue)
    }
}

extension KeyedEncodingContainer {
    mutating func encode(_ value: UpperSnakeCaseRepresentable, forKey key: KeyedEncodingContainer<K>.Key) throws {
        try encode(value.upperSnakeCaseValue, forKey: key)
    }
}

// The following is copied verbatim from https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
// The only change is to call uppercased() on the encoded value as part of the return.
fileprivate func _upperSnakeCaseValue(_ stringKey: String) -> String {
    guard !stringKey.isEmpty else { return stringKey }

    var words : [Range<String.Index>] = []
    // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
    //
    // myProperty -> my_property
    // myURLProperty -> my_url_property
    //
    // We assume, per Swift naming conventions, that the first character of the key is lowercase.
    var wordStart = stringKey.startIndex
    var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex

    // Find next uppercase character
    while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
        let untilUpperCase = wordStart..<upperCaseRange.lowerBound
        words.append(untilUpperCase)

        // Find next lowercase character
        searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
        guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
            // There are no more lower case letters. Just end here.
            wordStart = searchRange.lowerBound
            break
        }

        // Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word
        let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
        if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
            // The next character after capital is a lower case character and therefore not a word boundary.
            // Continue searching for the next upper case for the boundary.
            wordStart = upperCaseRange.lowerBound
        } else {
            // There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
            let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
            words.append(upperCaseRange.lowerBound..<beforeLowerIndex)

            // Next word starts at the capital before the lowercase we just found
            wordStart = beforeLowerIndex
        }
        searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
    }
    words.append(wordStart..<searchRange.upperBound)
    let result = words.map({ (range) in
        return stringKey[range].lowercased()
    }).joined(separator: "_")
    return result.uppercased()
}

enum Snake: String, UpperSnakeCaseRepresentable, Encodable {
    case blackAdder
    case mamba
}

struct Test: Encodable {
    let testKey: String?
    let snake: Snake
}
let test = Test(testKey: "testValue", snake: .mamba)

let enumData = try! JSONEncoder().encode(test)
let json = String(data: enumData, encoding: .utf8)!
print(json)

1 Ответ

2 голосов
/ 19 июня 2019

Я думаю, что вы на самом деле ищете стратегию кодирования значений? Стратегия кодирования ключей изменяет способ кодирования ключей , а не способ их кодирования. Стратегия кодирования значений будет выглядеть примерно так: JSONDecoder dateDecodingStrategy, и вы ищете одну для перечислений.

Это подход, который может работать для вас:

protocol UpperSnakeCaseRepresentable {
  var upperSnakeCaseValue: String { get }
}

extension UpperSnakeCaseRepresentable where Self: RawRepresentable, RawValue == String {
  var upperSnakeCaseValue: String {
    // Correct implementation left as an exercise
    return rawValue.uppercased()
  }
}

extension KeyedEncodingContainer {
  mutating func encode(_ value: UpperSnakeCaseRepresentable, forKey key: KeyedEncodingContainer<K>.Key) throws {
    try encode(value.upperSnakeCaseValue, forKey: key)
  }
}

enum Snake: String, UpperSnakeCaseRepresentable, Encodable {
  case blackAdder
  case mamba
}

struct Test: Encodable {
  let snake: Snake
}
let test = Test(snake: .blackAdder)

let data = try! JSONEncoder().encode(test)
let json = String(data: data, encoding: .utf8)!
print(json)

Теперь все перечисления, которые вы объявляете соответствующими UpperSnakeCaseRepresentable, будут кодироваться так, как вы хотите.

Вы можете расширить другие контейнеры кодирования и декодирования таким же образом.

...