Skip to content

Commit

Permalink
XMLEncoder overlay changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ShawnMoore committed Sep 29, 2018
1 parent acfeee5 commit 1ea1496
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 30 deletions.
98 changes: 84 additions & 14 deletions Sources/XMLParsing/Decoder/XMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,75 @@ open class XMLDecoder {
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}

/// The strategy to use for automatically changing the value of keys before decoding.
public enum KeyDecodingStrategy {
/// Use the keys specified by each type. This is the default strategy.
case useDefaultKeys

/// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
///
/// The conversion to upper 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 snake case to camel case:
/// 1. Capitalizes the word starting after each `_`
/// 2. Removes all `_`
/// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
/// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
///
/// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
case convertFromSnakeCase

/// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
/// The full path to the current decoding 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 decoding.
/// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from.
case custom((_ codingPath: [CodingKey]) -> CodingKey)

internal static func _convertFromSnakeCase(_ stringKey: String) -> String {
guard !stringKey.isEmpty else { return stringKey }

// Find the first non-underscore character
guard let firstNonUnderscore = stringKey.index(where: { $0 != "_" }) else {
// Reached the end without finding an _
return stringKey
}

// Find the last non-underscore character
var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
stringKey.formIndex(before: &lastNonUnderscore)
}

let keyRange = firstNonUnderscore...lastNonUnderscore
let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex

var components = stringKey[keyRange].split(separator: "_")
let joinedString : String
if components.count == 1 {
// No underscores in key, leave the word as is - maybe already camel cased
joinedString = String(stringKey[keyRange])
} else {
joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
}

// Do a cheap isEmpty check before creating and appending potentially empty strings
let result : String
if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
result = joinedString
} else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
// Both leading and trailing underscores
result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
} else if (!leadingUnderscoreRange.isEmpty) {
// Just leading
result = String(stringKey[leadingUnderscoreRange]) + joinedString
} else {
// Just trailing
result = joinedString + String(stringKey[trailingUnderscoreRange])
}
return result
}
}

/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970

Expand All @@ -111,6 +180,9 @@ open class XMLDecoder {
/// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw

/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey : Any] = [:]

Expand All @@ -119,6 +191,7 @@ open class XMLDecoder {
let dateDecodingStrategy: DateDecodingStrategy
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let keyDecodingStrategy: KeyDecodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -127,6 +200,7 @@ open class XMLDecoder {
return _Options(dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
userInfo: userInfo)
}

Expand Down Expand Up @@ -641,9 +715,8 @@ extension _XMLDecoder {
switch self.options.dateDecodingStrategy {
case .deferredToDate:
self.storage.push(container: value)
let date = try Date(from: self)
self.storage.popContainer()
return date
defer { self.storage.popContainer() }
return try Date(from: self)

case .secondsSince1970:
let double = try self.unbox(value, as: Double.self)!
Expand Down Expand Up @@ -675,9 +748,8 @@ extension _XMLDecoder {

case .custom(let closure):
self.storage.push(container: value)
let date = try closure(self)
self.storage.popContainer()
return date
defer { self.storage.popContainer() }
return try closure(self)
}
}

Expand All @@ -687,9 +759,8 @@ extension _XMLDecoder {
switch self.options.dataDecodingStrategy {
case .deferredToData:
self.storage.push(container: value)
let data = try Data(from: self)
self.storage.popContainer()
return data
defer { self.storage.popContainer() }
return try Data(from: self)

case .base64:
guard let string = value as? String else {
Expand All @@ -704,9 +775,8 @@ extension _XMLDecoder {

case .custom(let closure):
self.storage.push(container: value)
let data = try closure(self)
self.storage.popContainer()
return data
defer { self.storage.popContainer() }
return try closure(self)
}
}

Expand Down Expand Up @@ -742,8 +812,8 @@ extension _XMLDecoder {
decoded = decimal as! T
} else {
self.storage.push(container: value)
decoded = try type.init(from: self)
self.storage.popContainer()
defer { self.storage.popContainer() }
return try type.init(from: self)
}

return decoded
Expand Down
Loading

0 comments on commit 1ea1496

Please sign in to comment.