Skip to content

Commit 0251957

Browse files
[SE-0320] Added protocol CodingKeyRepresentable (#34458)
1 parent c31a590 commit 0251957

File tree

4 files changed

+252
-10
lines changed

4 files changed

+252
-10
lines changed

stdlib/public/core/Codable.swift

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5510,23 +5510,102 @@ internal struct _DictionaryCodingKey: CodingKey {
55105510
internal let stringValue: String
55115511
internal let intValue: Int?
55125512

5513-
internal init?(stringValue: String) {
5513+
internal init(stringValue: String) {
55145514
self.stringValue = stringValue
55155515
self.intValue = Int(stringValue)
55165516
}
55175517

5518-
internal init?(intValue: Int) {
5518+
internal init(intValue: Int) {
55195519
self.stringValue = "\(intValue)"
55205520
self.intValue = intValue
55215521
}
5522+
5523+
fileprivate init(codingKey: CodingKey) {
5524+
self.stringValue = codingKey.stringValue
5525+
self.intValue = codingKey.intValue
5526+
}
5527+
}
5528+
5529+
/// A type that can be converted to and from a coding key.
5530+
///
5531+
/// With a `CodingKeyRepresentable` type, you can losslessly convert between a
5532+
/// custom type and a `CodingKey` type.
5533+
///
5534+
/// Conforming a type to `CodingKeyRepresentable` lets you opt in to encoding
5535+
/// and decoding `Dictionary` values keyed by the conforming type to and from
5536+
/// a keyed container, rather than encoding and decoding the dictionary as an
5537+
/// unkeyed container of alternating key-value pairs.
5538+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5539+
public protocol CodingKeyRepresentable {
5540+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5541+
var codingKey: CodingKey { get }
5542+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5543+
init?<T: CodingKey>(codingKey: T)
5544+
}
5545+
5546+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5547+
extension RawRepresentable where Self: CodingKeyRepresentable, RawValue == String {
5548+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5549+
public var codingKey: CodingKey {
5550+
_DictionaryCodingKey(stringValue: rawValue)
5551+
}
5552+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5553+
public init?<T: CodingKey>(codingKey: T) {
5554+
self.init(rawValue: codingKey.stringValue)
5555+
}
5556+
}
5557+
5558+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5559+
extension RawRepresentable where Self: CodingKeyRepresentable, RawValue == Int {
5560+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5561+
public var codingKey: CodingKey {
5562+
_DictionaryCodingKey(intValue: rawValue)
5563+
}
5564+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5565+
public init?<T: CodingKey>(codingKey: T) {
5566+
if let intValue = codingKey.intValue {
5567+
self.init(rawValue: intValue)
5568+
} else {
5569+
return nil
5570+
}
5571+
}
5572+
}
5573+
5574+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5575+
extension Int: CodingKeyRepresentable {
5576+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5577+
public var codingKey: CodingKey {
5578+
_DictionaryCodingKey(intValue: self)
5579+
}
5580+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5581+
public init?<T: CodingKey>(codingKey: T) {
5582+
if let intValue = codingKey.intValue {
5583+
self = intValue
5584+
} else {
5585+
return nil
5586+
}
5587+
}
5588+
}
5589+
5590+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5591+
extension String: CodingKeyRepresentable {
5592+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5593+
public var codingKey: CodingKey {
5594+
_DictionaryCodingKey(stringValue: self)
5595+
}
5596+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
5597+
public init?<T: CodingKey>(codingKey: T) {
5598+
self = codingKey.stringValue
5599+
}
55225600
}
55235601

55245602
extension Dictionary: Encodable where Key: Encodable, Value: Encodable {
55255603
/// Encodes the contents of this dictionary into the given encoder.
55265604
///
5527-
/// If the dictionary uses `String` or `Int` keys, the contents are encoded
5528-
/// in a keyed container. Otherwise, the contents are encoded as alternating
5529-
/// key-value pairs in an unkeyed container.
5605+
/// If the dictionary uses keys that are `String`, `Int`, or a type conforming
5606+
/// to `CodingKeyRepresentable`, the contents are encoded in a keyed container.
5607+
/// Otherwise, the contents are encoded as alternating key-value pairs in an
5608+
/// unkeyed container.
55305609
///
55315610
/// This function throws an error if any values are invalid for the given
55325611
/// encoder's format.
@@ -5537,16 +5616,26 @@ extension Dictionary: Encodable where Key: Encodable, Value: Encodable {
55375616
// Since the keys are already Strings, we can use them as keys directly.
55385617
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
55395618
for (key, value) in self {
5540-
let codingKey = _DictionaryCodingKey(stringValue: key as! String)!
5619+
let codingKey = _DictionaryCodingKey(stringValue: key as! String)
55415620
try container.encode(value, forKey: codingKey)
55425621
}
55435622
} else if Key.self == Int.self {
55445623
// Since the keys are already Ints, we can use them as keys directly.
55455624
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
55465625
for (key, value) in self {
5547-
let codingKey = _DictionaryCodingKey(intValue: key as! Int)!
5626+
let codingKey = _DictionaryCodingKey(intValue: key as! Int)
55485627
try container.encode(value, forKey: codingKey)
55495628
}
5629+
} else if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *),
5630+
Key.self is CodingKeyRepresentable.Type {
5631+
// Since the keys are CodingKeyRepresentable, we can use the `codingKey`
5632+
// to create `_DictionaryCodingKey` instances.
5633+
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
5634+
for (key, value) in self {
5635+
let codingKey = (key as! CodingKeyRepresentable).codingKey
5636+
let dictionaryCodingKey = _DictionaryCodingKey(codingKey: codingKey)
5637+
try container.encode(value, forKey: dictionaryCodingKey)
5638+
}
55505639
} else {
55515640
// Keys are Encodable but not Strings or Ints, so we cannot arbitrarily
55525641
// convert to keys. We can encode as an array of alternating key-value
@@ -5601,6 +5690,22 @@ extension Dictionary: Decodable where Key: Decodable, Value: Decodable {
56015690
let value = try container.decode(Value.self, forKey: key)
56025691
self[key.intValue! as! Key] = value
56035692
}
5693+
} else if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *),
5694+
let keyType = Key.self as? CodingKeyRepresentable.Type {
5695+
// The keys are CodingKeyRepresentable, so we should be able to expect
5696+
// a keyed container.
5697+
let container = try decoder.container(keyedBy: _DictionaryCodingKey.self)
5698+
for codingKey in container.allKeys {
5699+
guard let key: Key = keyType.init(codingKey: codingKey) as? Key else {
5700+
throw DecodingError.dataCorruptedError(
5701+
forKey: codingKey,
5702+
in: container,
5703+
debugDescription: "Could not convert key to type \(Key.self)"
5704+
)
5705+
}
5706+
let value: Value = try container.decode(Value.self, forKey: codingKey)
5707+
self[key] = value
5708+
}
56045709
} else {
56055710
// We should have encoded as an array of alternating key-value pairs.
56065711
var container = try decoder.unkeyedContainer()

test/api-digester/Outputs/cake-abi.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,14 @@
18731873
"usr": "s:Se",
18741874
"mangledName": "$sSe"
18751875
},
1876+
{
1877+
"kind": "Conformance",
1878+
"name": "CodingKeyRepresentable",
1879+
"printedName": "CodingKeyRepresentable",
1880+
"usr": "s:s22CodingKeyRepresentableP",
1881+
"mangledName": "$ss22CodingKeyRepresentableP",
1882+
"isABIPlaceholder": true
1883+
},
18761884
{
18771885
"kind": "Conformance",
18781886
"name": "CustomReflectable",

test/api-digester/Outputs/cake.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,14 @@
17401740
"usr": "s:Se",
17411741
"mangledName": "$sSe"
17421742
},
1743+
{
1744+
"kind": "Conformance",
1745+
"name": "CodingKeyRepresentable",
1746+
"printedName": "CodingKeyRepresentable",
1747+
"usr": "s:s22CodingKeyRepresentableP",
1748+
"mangledName": "$ss22CodingKeyRepresentableP",
1749+
"isABIPlaceholder": true
1750+
},
17431751
{
17441752
"kind": "Conformance",
17451753
"name": "CustomReflectable",

test/stdlib/CodableTests.swift

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,23 @@ func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Dat
7676
expectEqual(value, decoded, "\(#file):\(lineNumber): Decoded \(T.self) <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
7777
}
7878

79-
func expectRoundTripEqualityThroughJSON<T : Codable>(for value: T, lineNumber: Int) where T : Equatable {
79+
func expectRoundTripEqualityThroughJSON<T : Codable>(for value: T, expectedJSON: String? = nil, lineNumber: Int) where T : Equatable {
8080
let inf = "INF", negInf = "-INF", nan = "NaN"
8181
let encode = { (_ value: T) throws -> Data in
8282
let encoder = JSONEncoder()
8383
encoder.nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: inf,
8484
negativeInfinity: negInf,
8585
nan: nan)
86-
return try encoder.encode(value)
86+
if #available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
87+
encoder.outputFormatting = .sortedKeys
88+
}
89+
let encoded = try encoder.encode(value)
90+
91+
if let expectedJSON = expectedJSON {
92+
let actualJSON = String(decoding: encoded, as: UTF8.self)
93+
expectEqual(expectedJSON, actualJSON, line: UInt(lineNumber))
94+
}
95+
return encoded
8796
}
8897

8998
let decode = { (_ type: T.Type, _ data: Data) throws -> T in
@@ -111,13 +120,22 @@ func expectRoundTripEqualityThroughPlist<T : Codable>(for value: T, lineNumber:
111120

112121
// MARK: - Helper Types
113122
// A wrapper around a UUID that will allow it to be encoded at the top level of an encoder.
114-
struct UUIDCodingWrapper : Codable, Equatable {
123+
struct UUIDCodingWrapper : Codable, Equatable, Hashable, CodingKeyRepresentable {
115124
let value: UUID
116125

117126
init(_ value: UUID) {
118127
self.value = value
119128
}
120129

130+
init?<T: CodingKey>(codingKey: T) {
131+
guard let uuid = UUID(uuidString: codingKey.stringValue) else { return nil }
132+
self.value = uuid
133+
}
134+
135+
var codingKey: CodingKey {
136+
GenericCodingKey(stringValue: value.uuidString)
137+
}
138+
121139
static func ==(_ lhs: UUIDCodingWrapper, _ rhs: UUIDCodingWrapper) -> Bool {
122140
return lhs.value == rhs.value
123141
}
@@ -462,6 +480,91 @@ class TestCodable : TestCodableSuper {
462480
}
463481
}
464482

483+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
484+
func test_Dictionary_JSON() {
485+
enum X: String, Codable { case a, b }
486+
enum Y: String, Codable, CodingKeyRepresentable { case a, b }
487+
enum Z: Codable, CodingKeyRepresentable {
488+
case a
489+
case b
490+
init?<T: CodingKey>(codingKey: T) {
491+
switch codingKey.stringValue {
492+
case "α":
493+
self = .a
494+
case "β":
495+
self = .b
496+
default:
497+
return nil
498+
}
499+
}
500+
501+
var codingKey: CodingKey {
502+
GenericCodingKey(stringValue: encoded)
503+
}
504+
505+
var encoded: String {
506+
switch self {
507+
case .a: return "α"
508+
case .b: return "β"
509+
}
510+
}
511+
}
512+
enum S: Character, Codable, CodingKeyRepresentable {
513+
case a = "a"
514+
case b = "b"
515+
516+
init?<T: CodingKey>(codingKey: T) {
517+
guard codingKey.stringValue.count == 1 else { return nil }
518+
self.init(rawValue: codingKey.stringValue.first!)
519+
}
520+
521+
var codingKey: CodingKey {
522+
GenericCodingKey(stringValue: "\(self.rawValue)")
523+
}
524+
}
525+
526+
enum U: Int, Codable { case a = 0, b}
527+
enum V: Int, Codable, CodingKeyRepresentable { case a = 0, b }
528+
enum W: Codable, CodingKeyRepresentable {
529+
case a
530+
case b
531+
init?<T: CodingKey>(codingKey: T) {
532+
guard let intValue = codingKey.intValue else { return nil }
533+
switch intValue {
534+
case 42:
535+
self = .a
536+
case 64:
537+
self = .b
538+
default:
539+
return nil
540+
}
541+
}
542+
var codingKey: CodingKey {
543+
GenericCodingKey(intValue: self.encoded)
544+
}
545+
var encoded: Int {
546+
switch self {
547+
case .a: return 42
548+
case .b: return 64
549+
}
550+
}
551+
}
552+
553+
let uuid = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!
554+
let uuidWrapper = UUIDCodingWrapper(uuid)
555+
556+
expectRoundTripEqualityThroughJSON(for: [X.a: true], expectedJSON: #"["a",true]"#, lineNumber: #line)
557+
expectRoundTripEqualityThroughJSON(for: [Y.a: true, Y.b: false], expectedJSON: #"{"a":true,"b":false}"#, lineNumber: #line)
558+
expectRoundTripEqualityThroughJSON(for: [Z.a: true, Z.b: false], expectedJSON: #"{"α":true,"β":false}"#, lineNumber: #line)
559+
expectRoundTripEqualityThroughJSON(for: [S.a: true, S.b: false], expectedJSON: #"{"a":true,"b":false}"#, lineNumber: #line)
560+
expectRoundTripEqualityThroughJSON(for: [uuidWrapper: true], expectedJSON: #"{"E621E1F8-C36C-495A-93FC-0C247A3E6E5F":true}"#, lineNumber: #line)
561+
expectRoundTripEqualityThroughJSON(for: [uuid: true], expectedJSON: #"["E621E1F8-C36C-495A-93FC-0C247A3E6E5F",true]"#, lineNumber: #line)
562+
expectRoundTripEqualityThroughJSON(for: [U.a: true], expectedJSON: #"[0,true]"#, lineNumber: #line)
563+
expectRoundTripEqualityThroughJSON(for: [V.a: true, V.b: false], expectedJSON: #"{"0":true,"1":false}"#, lineNumber: #line)
564+
expectRoundTripEqualityThroughJSON(for: [W.a: true, W.b: false], expectedJSON: #"{"42":true,"64":false}"#, lineNumber: #line)
565+
}
566+
567+
465568
// MARK: - IndexPath
466569
lazy var indexPathValues: [Int : IndexPath] = [
467570
#line : IndexPath(), // empty
@@ -853,6 +956,20 @@ class TestCodable : TestCodableSuper {
853956

854957
// MARK: - Helper Types
855958

959+
struct GenericCodingKey: CodingKey {
960+
var stringValue: String
961+
var intValue: Int?
962+
963+
init(stringValue: String) {
964+
self.stringValue = stringValue
965+
}
966+
967+
init(intValue: Int) {
968+
self.stringValue = "\(intValue)"
969+
self.intValue = intValue
970+
}
971+
}
972+
856973
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
857974
let value: T
858975

@@ -937,6 +1054,10 @@ if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
9371054
tests["test_URLComponents_Plist"] = TestCodable.test_URLComponents_Plist
9381055
}
9391056

1057+
if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *) {
1058+
tests["test_Dictionary_JSON"] = TestCodable.test_Dictionary_JSON
1059+
}
1060+
9401061
var CodableTests = TestSuite("TestCodable")
9411062
for (name, test) in tests {
9421063
CodableTests.test(name) { test(TestCodable())() }

0 commit comments

Comments
 (0)