Skip to content

[5.0] Implement SE-0239: Add Codable conformance to Range types #21857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,26 @@ extension ClosedRange {
// shorthand. TODO: Add documentation
public typealias CountableClosedRange<Bound: Strideable> = ClosedRange<Bound>
where Bound.Stride : SignedInteger

extension ClosedRange: Decodable where Bound: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let lowerBound = try container.decode(Bound.self)
let upperBound = try container.decode(Bound.self)
guard lowerBound <= upperBound else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Cannot initialize \(ClosedRange.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
}
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
}

extension ClosedRange: Encodable where Bound: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.lowerBound)
try container.encode(self.upperBound)
}
}
34 changes: 34 additions & 0 deletions stdlib/public/core/Codable.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,40 @@ extension Array : Decodable where Element : Decodable {
}
}

extension ContiguousArray : Encodable where Element : Encodable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be some tests for the ContiguousArray extensions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there should. This is being discussed in the comments to
#20715. I'll update the PR once that's done.

/// Encodes the elements of this contiguous array into the given encoder
/// in an unkeyed container.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in self {
try container.encode(element)
}
}
}

extension ContiguousArray : Decodable where Element : Decodable {
/// Creates a new contiguous array by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
public init(from decoder: Decoder) throws {
self.init()

var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
let element = try container.decode(Element.self)
self.append(element)
}
}
}

extension Set : Encodable where Element : Encodable {
/// Encodes the elements of this set into the given encoder in an unkeyed
/// container.
Expand Down
65 changes: 65 additions & 0 deletions stdlib/public/core/Range.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,29 @@ extension Range: Hashable where Bound: Hashable {
}
}

extension Range: Decodable where Bound: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let lowerBound = try container.decode(Bound.self)
let upperBound = try container.decode(Bound.self)
guard lowerBound <= upperBound else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Cannot initialize \(Range.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
}
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
}

extension Range: Encodable where Bound: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.lowerBound)
try container.encode(self.upperBound)
}
}

/// A partial half-open interval up to, but not including, an upper bound.
///
/// You create `PartialRangeUpTo` instances by using the prefix half-open range
Expand Down Expand Up @@ -447,6 +470,20 @@ extension PartialRangeUpTo: RangeExpression {
}
}

extension PartialRangeUpTo: Decodable where Bound: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
try self.init(container.decode(Bound.self))
}
}

extension PartialRangeUpTo: Encodable where Bound: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.upperBound)
}
}

/// A partial interval up to, and including, an upper bound.
///
/// You create `PartialRangeThrough` instances by using the prefix closed range
Expand Down Expand Up @@ -488,6 +525,20 @@ extension PartialRangeThrough: RangeExpression {
}
}

extension PartialRangeThrough: Decodable where Bound: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
try self.init(container.decode(Bound.self))
}
}

extension PartialRangeThrough: Encodable where Bound: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.upperBound)
}
}

/// A partial interval extending upward from a lower bound.
///
/// You create `PartialRangeFrom` instances by using the postfix range operator
Expand Down Expand Up @@ -624,6 +675,20 @@ extension PartialRangeFrom: Sequence
}
}

extension PartialRangeFrom: Decodable where Bound: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
try self.init(container.decode(Bound.self))
}
}

extension PartialRangeFrom: Encodable where Bound: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.lowerBound)
}
}

extension Comparable {
/// Returns a half-open range that contains its lower bound but not its upper
/// bound.
Expand Down
98 changes: 91 additions & 7 deletions test/stdlib/CodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,25 @@ func debugDescription<T>(_ value: T) -> String {
}
}

func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (Data) throws -> T, lineNumber: Int) where T : Equatable {
func performEncodeAndDecode<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (T.Type, Data) throws -> T, lineNumber: Int) -> T {

let data: Data
do {
data = try encode(value)
} catch {
fatalError("\(#file):\(lineNumber): Unable to encode \(T.self) <\(debugDescription(value))>: \(error)")
}

let decoded: T
do {
decoded = try decode(data)
return try decode(T.self, data)
} catch {
fatalError("\(#file):\(lineNumber): Unable to decode \(T.self) <\(debugDescription(value))>: \(error)")
}
}

func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Data, decode: (T.Type, Data) throws -> T, lineNumber: Int) where T : Equatable {

let decoded = performEncodeAndDecode(of: value, encode: encode, decode: decode, lineNumber: lineNumber)

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

let decode = { (_ data: Data) throws -> T in
let decode = { (_ type: T.Type, _ data: Data) throws -> T in
let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: inf,
negativeInfinity: negInf,
nan: nan)
return try decoder.decode(T.self, from: data)
return try decoder.decode(type, from: data)
}

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

let decode = { (_ data: Data) throws -> T in
return try PropertyListDecoder().decode(T.self, from: data)
let decode = { (_ type: T.Type,_ data: Data) throws -> T in
return try PropertyListDecoder().decode(type, from: data)
}

expectRoundTripEquality(of: value, encode: encode, decode: decode, lineNumber: lineNumber)
Expand Down Expand Up @@ -351,6 +356,21 @@ class TestCodable : TestCodableSuper {
}
}

// MARK: - ClosedRange
func test_ClosedRange_JSON() {
let value = 0...Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded ClosedRange upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded ClosedRange lowerBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

func test_ClosedRange_Plist() {
let value = 0...Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded ClosedRange upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded ClosedRange lowerBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

// MARK: - DateComponents
lazy var dateComponents: Set<Calendar.Component> = [
.era, .year, .month, .day, .hour, .minute, .second, .nanosecond,
Expand Down Expand Up @@ -545,6 +565,45 @@ class TestCodable : TestCodableSuper {
}
}

// MARK: - PartialRangeFrom
func test_PartialRangeFrom_JSON() {
let value = 0...
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded PartialRangeFrom <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

func test_PartialRangeFrom_Plist() {
let value = 0...
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded PartialRangeFrom <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

// MARK: - PartialRangeThrough
func test_PartialRangeThrough_JSON() {
let value = ...Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeThrough <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

func test_PartialRangeThrough_Plist() {
let value = ...Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeThrough <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

// MARK: - PartialRangeUpTo
func test_PartialRangeUpTo_JSON() {
let value = ..<Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeUpTo <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

func test_PartialRangeUpTo_Plist() {
let value = ..<Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded PartialRangeUpTo <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

// MARK: - PersonNameComponents
@available(macOS 10.11, iOS 9.0, watchOS 2.0, tvOS 9.0, *)
lazy var personNameComponentsValues: [Int : PersonNameComponents] = [
Expand All @@ -567,6 +626,21 @@ class TestCodable : TestCodableSuper {
}
}

// MARK: - Range
func test_Range_JSON() {
let value = 0..<Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try JSONEncoder().encode($0) }, decode: { try JSONDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded Range upperBound <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded Range lowerBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

func test_Range_Plist() {
let value = 0..<Int.max
let decoded = performEncodeAndDecode(of: value, encode: { try PropertyListEncoder().encode($0) }, decode: { try PropertyListDecoder().decode($0, from: $1) }, lineNumber: #line)
expectEqual(value.upperBound, decoded.upperBound, "\(#file):\(#line): Decoded Range upperBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
expectEqual(value.lowerBound, decoded.lowerBound, "\(#file):\(#line): Decoded Range lowerBound<\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
}

// MARK: - TimeZone
lazy var timeZoneValues: [Int : TimeZone] = [
#line : TimeZone(identifier: "America/Los_Angeles")!,
Expand Down Expand Up @@ -777,6 +851,8 @@ var tests = [
"test_CGRect_Plist" : TestCodable.test_CGRect_Plist,
"test_CGVector_JSON" : TestCodable.test_CGVector_JSON,
"test_CGVector_Plist" : TestCodable.test_CGVector_Plist,
"test_ClosedRange_JSON" : TestCodable.test_ClosedRange_JSON,
"test_ClosedRange_Plist" : TestCodable.test_ClosedRange_Plist,
"test_DateComponents_JSON" : TestCodable.test_DateComponents_JSON,
"test_DateComponents_Plist" : TestCodable.test_DateComponents_Plist,
"test_Decimal_JSON" : TestCodable.test_Decimal_JSON,
Expand All @@ -789,6 +865,14 @@ var tests = [
"test_Locale_Plist" : TestCodable.test_Locale_Plist,
"test_NSRange_JSON" : TestCodable.test_NSRange_JSON,
"test_NSRange_Plist" : TestCodable.test_NSRange_Plist,
"test_PartialRangeFrom_JSON" : TestCodable.test_PartialRangeFrom_JSON,
"test_PartialRangeFrom_Plist" : TestCodable.test_PartialRangeFrom_Plist,
"test_PartialRangeThrough_JSON" : TestCodable.test_PartialRangeThrough_JSON,
"test_PartialRangeThrough_Plist" : TestCodable.test_PartialRangeThrough_Plist,
"test_PartialRangeUpTo_JSON" : TestCodable.test_PartialRangeUpTo_JSON,
"test_PartialRangeUpTo_Plist" : TestCodable.test_PartialRangeUpTo_Plist,
"test_Range_JSON" : TestCodable.test_Range_JSON,
"test_Range_Plist" : TestCodable.test_Range_Plist,
"test_TimeZone_JSON" : TestCodable.test_TimeZone_JSON,
"test_TimeZone_Plist" : TestCodable.test_TimeZone_Plist,
"test_URL_JSON" : TestCodable.test_URL_JSON,
Expand Down