Skip to content

Commit 3b6c6cc

Browse files
authored
Merge pull request #19532 from dlbuckley/SR-8649_ranges_codable
SR-8649: Range types conform to Codable
2 parents 69c622f + 6ef7cca commit 3b6c6cc

File tree

3 files changed

+179
-7
lines changed

3 files changed

+179
-7
lines changed

stdlib/public/core/ClosedRange.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,26 @@ extension ClosedRange {
456456
// shorthand. TODO: Add documentation
457457
public typealias CountableClosedRange<Bound: Strideable> = ClosedRange<Bound>
458458
where Bound.Stride : SignedInteger
459+
460+
extension ClosedRange: Decodable where Bound: Decodable {
461+
public init(from decoder: Decoder) throws {
462+
var container = try decoder.unkeyedContainer()
463+
let lowerBound = try container.decode(Bound.self)
464+
let upperBound = try container.decode(Bound.self)
465+
guard lowerBound <= upperBound else {
466+
throw DecodingError.dataCorrupted(
467+
DecodingError.Context(
468+
codingPath: decoder.codingPath,
469+
debugDescription: "Cannot initialize \(ClosedRange.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
470+
}
471+
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
472+
}
473+
}
474+
475+
extension ClosedRange: Encodable where Bound: Encodable {
476+
public func encode(to encoder: Encoder) throws {
477+
var container = encoder.unkeyedContainer()
478+
try container.encode(self.lowerBound)
479+
try container.encode(self.upperBound)
480+
}
481+
}

stdlib/public/core/Range.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,29 @@ extension Range: Hashable where Bound: Hashable {
405405
}
406406
}
407407

408+
extension Range: Decodable where Bound: Decodable {
409+
public init(from decoder: Decoder) throws {
410+
var container = try decoder.unkeyedContainer()
411+
let lowerBound = try container.decode(Bound.self)
412+
let upperBound = try container.decode(Bound.self)
413+
guard lowerBound <= upperBound else {
414+
throw DecodingError.dataCorrupted(
415+
DecodingError.Context(
416+
codingPath: decoder.codingPath,
417+
debugDescription: "Cannot initialize \(Range.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
418+
}
419+
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
420+
}
421+
}
422+
423+
extension Range: Encodable where Bound: Encodable {
424+
public func encode(to encoder: Encoder) throws {
425+
var container = encoder.unkeyedContainer()
426+
try container.encode(self.lowerBound)
427+
try container.encode(self.upperBound)
428+
}
429+
}
430+
408431
/// A partial half-open interval up to, but not including, an upper bound.
409432
///
410433
/// You create `PartialRangeUpTo` instances by using the prefix half-open range
@@ -447,6 +470,20 @@ extension PartialRangeUpTo: RangeExpression {
447470
}
448471
}
449472

473+
extension PartialRangeUpTo: Decodable where Bound: Decodable {
474+
public init(from decoder: Decoder) throws {
475+
var container = try decoder.unkeyedContainer()
476+
try self.init(container.decode(Bound.self))
477+
}
478+
}
479+
480+
extension PartialRangeUpTo: Encodable where Bound: Encodable {
481+
public func encode(to encoder: Encoder) throws {
482+
var container = encoder.unkeyedContainer()
483+
try container.encode(self.upperBound)
484+
}
485+
}
486+
450487
/// A partial interval up to, and including, an upper bound.
451488
///
452489
/// You create `PartialRangeThrough` instances by using the prefix closed range
@@ -488,6 +525,20 @@ extension PartialRangeThrough: RangeExpression {
488525
}
489526
}
490527

528+
extension PartialRangeThrough: Decodable where Bound: Decodable {
529+
public init(from decoder: Decoder) throws {
530+
var container = try decoder.unkeyedContainer()
531+
try self.init(container.decode(Bound.self))
532+
}
533+
}
534+
535+
extension PartialRangeThrough: Encodable where Bound: Encodable {
536+
public func encode(to encoder: Encoder) throws {
537+
var container = encoder.unkeyedContainer()
538+
try container.encode(self.upperBound)
539+
}
540+
}
541+
491542
/// A partial interval extending upward from a lower bound.
492543
///
493544
/// You create `PartialRangeFrom` instances by using the postfix range operator
@@ -624,6 +675,20 @@ extension PartialRangeFrom: Sequence
624675
}
625676
}
626677

678+
extension PartialRangeFrom: Decodable where Bound: Decodable {
679+
public init(from decoder: Decoder) throws {
680+
var container = try decoder.unkeyedContainer()
681+
try self.init(container.decode(Bound.self))
682+
}
683+
}
684+
685+
extension PartialRangeFrom: Encodable where Bound: Encodable {
686+
public func encode(to encoder: Encoder) throws {
687+
var container = encoder.unkeyedContainer()
688+
try container.encode(self.lowerBound)
689+
}
690+
}
691+
627692
extension Comparable {
628693
/// Returns a half-open range that contains its lower bound but not its upper
629694
/// bound.

test/stdlib/CodableTests.swift

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,25 @@ func debugDescription<T>(_ value: T) -> String {
4949
}
5050
}
5151

52-
func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (Data) throws -> T, lineNumber: Int) where T : Equatable {
52+
func performEncodeAndDecode<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (T.Type, Data) throws -> T, lineNumber: Int) -> T {
53+
5354
let data: Data
5455
do {
5556
data = try encode(value)
5657
} catch {
5758
fatalError("\(#file):\(lineNumber): Unable to encode \(T.self) <\(debugDescription(value))>: \(error)")
5859
}
5960

60-
let decoded: T
6161
do {
62-
decoded = try decode(data)
62+
return try decode(T.self, data)
6363
} catch {
6464
fatalError("\(#file):\(lineNumber): Unable to decode \(T.self) <\(debugDescription(value))>: \(error)")
6565
}
66+
}
67+
68+
func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (T.Type, Data) throws -> T, lineNumber: Int) where T : Equatable {
69+
70+
let decoded = performEncodeAndDecode(of: value, encode: encode, decode: decode, lineNumber: lineNumber)
6671

6772
expectEqual(value, decoded, "\(#file):\(lineNumber): Decoded \(T.self) <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
6873
}
@@ -77,12 +82,12 @@ func expectRoundTripEqualityThroughJSON<T : Codable>(for value: T, lineNumber: I
7782
return try encoder.encode(value)
7883
}
7984

80-
let decode = { (_ data: Data) throws -> T in
85+
let decode = { (_ type: T.Type, _ data: Data) throws -> T in
8186
let decoder = JSONDecoder()
8287
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: inf,
8388
negativeInfinity: negInf,
8489
nan: nan)
85-
return try decoder.decode(T.self, from: data)
90+
return try decoder.decode(type, from: data)
8691
}
8792

8893
expectRoundTripEquality(of: value, encode: encode, decode: decode, lineNumber: lineNumber)
@@ -93,8 +98,8 @@ func expectRoundTripEqualityThroughPlist<T : Codable>(for value: T, lineNumber:
9398
return try PropertyListEncoder().encode(value)
9499
}
95100

96-
let decode = { (_ data: Data) throws -> T in
97-
return try PropertyListDecoder().decode(T.self, from: data)
101+
let decode = { (_ type: T.Type,_ data: Data) throws -> T in
102+
return try PropertyListDecoder().decode(type, from: data)
98103
}
99104

100105
expectRoundTripEquality(of: value, encode: encode, decode: decode, lineNumber: lineNumber)
@@ -351,6 +356,21 @@ class TestCodable : TestCodableSuper {
351356
}
352357
}
353358

359+
// MARK: - ClosedRange
360+
func test_ClosedRange_JSON() {
361+
let value = 0...Int.max
362+
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
363+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded ClosedRange upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
364+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded ClosedRange lowerBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
365+
}
366+
367+
func test_ClosedRange_Plist() {
368+
let value = 0...Int.max
369+
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
370+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded ClosedRange upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
371+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded ClosedRange lowerBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
372+
}
373+
354374
// MARK: - DateComponents
355375
lazy var dateComponents: Set<Calendar.Component> = [
356376
.era, .year, .month, .day, .hour, .minute, .second, .nanosecond,
@@ -545,6 +565,45 @@ class TestCodable : TestCodableSuper {
545565
}
546566
}
547567

568+
// MARK: - PartialRangeFrom
569+
func test_PartialRangeFrom_JSON() {
570+
let value = 0...
571+
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
572+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded PartialRangeFrom <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
573+
}
574+
575+
func test_PartialRangeFrom_Plist() {
576+
let value = 0...
577+
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
578+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded PartialRangeFrom <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
579+
}
580+
581+
// MARK: - PartialRangeThrough
582+
func test_PartialRangeThrough_JSON() {
583+
let value = ...Int.max
584+
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
585+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeThrough <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
586+
}
587+
588+
func test_PartialRangeThrough_Plist() {
589+
let value = ...Int.max
590+
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
591+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeThrough <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
592+
}
593+
594+
// MARK: - PartialRangeUpTo
595+
func test_PartialRangeUpTo_JSON() {
596+
let value = ..<Int.max
597+
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
598+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeUpTo <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
599+
}
600+
601+
func test_PartialRangeUpTo_Plist() {
602+
let value = ..<Int.max
603+
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
604+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeUpTo <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
605+
}
606+
548607
// MARK: - PersonNameComponents
549608
@available(macOS 10.11, iOS 9.0, watchOS 2.0, tvOS 9.0, *)
550609
lazy var personNameComponentsValues: [Int : PersonNameComponents] = [
@@ -567,6 +626,21 @@ class TestCodable : TestCodableSuper {
567626
}
568627
}
569628

629+
// MARK: - Range
630+
func test_Range_JSON() {
631+
let value = 0..<Int.max
632+
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
633+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded Range upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
634+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded Range lowerBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
635+
}
636+
637+
func test_Range_Plist() {
638+
let value = 0..<Int.max
639+
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
640+
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded Range upperBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
641+
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded Range lowerBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
642+
}
643+
570644
// MARK: - TimeZone
571645
lazy var timeZoneValues: [Int : TimeZone] = [
572646
#line : TimeZone(identifier: "America/Los_Angeles")!,
@@ -777,6 +851,8 @@ var tests = [
777851
"test_CGRect_Plist" : TestCodable.test_CGRect_Plist,
778852
"test_CGVector_JSON" : TestCodable.test_CGVector_JSON,
779853
"test_CGVector_Plist" : TestCodable.test_CGVector_Plist,
854+
"test_ClosedRange_JSON" : TestCodable.test_ClosedRange_JSON,
855+
"test_ClosedRange_Plist" : TestCodable.test_ClosedRange_Plist,
780856
"test_DateComponents_JSON" : TestCodable.test_DateComponents_JSON,
781857
"test_DateComponents_Plist" : TestCodable.test_DateComponents_Plist,
782858
"test_Decimal_JSON" : TestCodable.test_Decimal_JSON,
@@ -789,6 +865,14 @@ var tests = [
789865
"test_Locale_Plist" : TestCodable.test_Locale_Plist,
790866
"test_NSRange_JSON" : TestCodable.test_NSRange_JSON,
791867
"test_NSRange_Plist" : TestCodable.test_NSRange_Plist,
868+
"test_PartialRangeFrom_JSON" : TestCodable.test_PartialRangeFrom_JSON,
869+
"test_PartialRangeFrom_Plist" : TestCodable.test_PartialRangeFrom_Plist,
870+
"test_PartialRangeThrough_JSON" : TestCodable.test_PartialRangeThrough_JSON,
871+
"test_PartialRangeThrough_Plist" : TestCodable.test_PartialRangeThrough_Plist,
872+
"test_PartialRangeUpTo_JSON" : TestCodable.test_PartialRangeUpTo_JSON,
873+
"test_PartialRangeUpTo_Plist" : TestCodable.test_PartialRangeUpTo_Plist,
874+
"test_Range_JSON" : TestCodable.test_Range_JSON,
875+
"test_Range_Plist" : TestCodable.test_Range_Plist,
792876
"test_TimeZone_JSON" : TestCodable.test_TimeZone_JSON,
793877
"test_TimeZone_Plist" : TestCodable.test_TimeZone_Plist,
794878
"test_URL_JSON" : TestCodable.test_URL_JSON,

0 commit comments

Comments
 (0)