From c09a243dce09d627009bf9782785241f6d04e773 Mon Sep 17 00:00:00 2001 From: arthurgau0419 Date: Fri, 18 Jun 2021 14:22:55 +0800 Subject: [PATCH] KeyEncodingStrategy feature inspired by JsonEncoder. --- Sources/ObjectEncoder/Encoder.swift | 113 ++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/Sources/ObjectEncoder/Encoder.swift b/Sources/ObjectEncoder/Encoder.swift index a349e22..9967e30 100644 --- a/Sources/ObjectEncoder/Encoder.swift +++ b/Sources/ObjectEncoder/Encoder.swift @@ -47,10 +47,17 @@ public struct ObjectEncoder { set { options.encodingStrategies = newValue } } + /// The strategies to use for encoding keys. + public var keyEncodingStrategy: KeyEncodingStrategy { + get { return options.keyEncodingStrategy } + set { options.keyEncodingStrategy = newValue } + } + // MARK: - fileprivate struct Options { fileprivate var encodingStrategies = EncodingStrategies() + fileprivate var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys } fileprivate var options = Options() @@ -133,6 +140,84 @@ extension ObjectEncoder.Encoder { } } +extension ObjectEncoder { + /// The strategy to use for automatically changing the value of keys before encoding. + public enum KeyEncodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys + + /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload. + /// + /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt). + /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. + /// + /// Converting from camel case to snake case: + /// 1. Splits words at the boundary of lower-case to upper-case + /// 2. Inserts `_` between words + /// 3. Lowercases the entire string + /// 4. Preserves starting and ending `_`. + /// + /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. + /// + /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. + case convertToSnakeCase + + /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types. + /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding. + /// If the result of the conversion is a duplicate key, then only one value will be present in the result. + case custom((_ codingPath: [CodingKey]) -> CodingKey) + + fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + + var words : [Range] = [] + // 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)..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..: KeyedEncodingContainerPr self.encoder = encoder } + // MARK: - Coding Path Operations + + private func _converted(_ key: CodingKey) -> CodingKey { + switch encoder.options.keyEncodingStrategy { + case .useDefaultKeys: + return key + case .convertToSnakeCase: + let newKeyString = ObjectEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue) + return _ObjectCodingKey(stringValue: newKeyString)! + case .custom(let converter): + return converter(codingPath + [key]) + } + } + // MARK: - Swift.KeyedEncodingContainerProtocol Methods var codingPath: [CodingKey] { return encoder.codingPath } - func encodeNil(forKey key: Key) throws { try encoder(for: key).encodeNil() } - func encode(_ value: T, forKey key: Key) throws where T: Primitive { try encoder(for: key).encode(value) } - func encode(_ value: T, forKey key: Key) throws where T: Encodable { try encoder(for: key).encode(value) } + func encodeNil(forKey key: Key) throws { try encoder(for: _converted(key)).encodeNil() } + func encode(_ value: T, forKey key: Key) throws where T: Primitive { try encoder(for: _converted(key)).encode(value) } + func encode(_ value: T, forKey key: Key) throws where T: Encodable { try encoder(for: _converted(key)).encode(value) } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { - return encoder(for: key).container(keyedBy: type) + return encoder(for: _converted(key)).container(keyedBy: type) } func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - return encoder(for: key).unkeyedContainer() + return encoder(for: _converted(key)).unkeyedContainer() } - func superEncoder() -> Encoder { return encoder(for: _ObjectCodingKey.super) } - func superEncoder(forKey key: Key) -> Encoder { return encoder(for: key) } + func superEncoder() -> Encoder { return encoder(for: _converted(_ObjectCodingKey.super)) } + func superEncoder(forKey key: Key) -> Encoder { return encoder(for: _converted(key)) } } private struct _UnkeyedEncodingContainer: UnkeyedEncodingContainer {