Skip to content

Commit

Permalink
Implement Data encoding/decoding strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
lilyball committed Feb 26, 2018
1 parent 05586c9 commit 65ef35b
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
* Add `JSON.Decoder.keyDecodingStrategy`. This is very similar to Swift 4.1's `JSONDecoder.keyDecodingStrategy`, although by default it won't apply to decoding any values of type `JSON` or `JSONObject` (there's another option `applyKeyDecodingStrategyToJSONObject` that controls this).
* Add `JSON.Encoder.dateEncodingStrategy`. This is very similar to `JSONEncoder.dateEncodingStrategy` except it includes another case for encoding ISO8601-formatted dates with fractional seconds (on Apple platforms).
* Add `JSON.Decoder.dateDecodingStrategy`. This is very similar to `JSONDecoder.dateDecodingStrategy` except it includes another case for decoding ISO8601-formatted dates with fractional seconds (on Apple platforms).
* Add `JSON.Encoder.dataEncodingStrategy`. This is identical to `JSONEncoder.dataEncodingStrategy`.
* Add `JSON.Decoder.dataDecodingStrategy`. This is identical to `JSONDecoder.dataDecodingStrategy`.

#### v3.0.2 (2018-02-21)

Expand Down
31 changes: 31 additions & 0 deletions Sources/Coders/SwiftDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ extension JSON {
/// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
public var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate

/// The strategy to use in decoding binary data. Defaults to `.base64`.
public var dataDecodingStrategy: DataDecodingStrategy = .base64

/// Creates a new, reusable JSON decoder.
public init() {}

Expand Down Expand Up @@ -91,6 +94,7 @@ extension JSON {
data.keyDecodingStrategy = keyDecodingStrategy
data.applyKeyDecodingStrategyToJSONObject = applyKeyDecodingStrategyToJSONObject
data.dateDecodingStrategy = dateDecodingStrategy
data.dataDecodingStrategy = dataDecodingStrategy
return try _JSONDecoder(data: data, value: json).decode(type)
}
}
Expand All @@ -102,6 +106,7 @@ private class DecoderData {
var keyDecodingStrategy: JSON.Decoder.KeyDecodingStrategy = .useDefaultKeys
var applyKeyDecodingStrategyToJSONObject = false
var dateDecodingStrategy: JSON.Decoder.DateDecodingStrategy = .deferredToDate
var dataDecodingStrategy: JSON.Decoder.DataDecodingStrategy = .base64

func copy() -> DecoderData {
let result = DecoderData()
Expand All @@ -110,6 +115,7 @@ private class DecoderData {
result.keyDecodingStrategy = keyDecodingStrategy
result.applyKeyDecodingStrategyToJSONObject = applyKeyDecodingStrategyToJSONObject
result.dateDecodingStrategy = dateDecodingStrategy
result.dataDecodingStrategy = dataDecodingStrategy
return result
}
}
Expand Down Expand Up @@ -364,6 +370,19 @@ extension _JSONDecoder: SingleValueDecodingContainer {
case .custom(let decode):
return try decode(self) as! T
}
case is Data.Type:
switch _data.dataDecodingStrategy {
case .deferredToData:
break
case .base64:
let str = try wrapTypeMismatch(value.asJSON.getString())
guard let data = Data(base64Encoded: str) else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Encountered Data is not valid Base64.")
}
return data as! T
case .custom(let decode):
return try decode(self) as! T
}
default:
break
}
Expand Down Expand Up @@ -811,6 +830,18 @@ extension JSON.Decoder {
/// Decode the `Date` as a custom value decoded by the given closure.
case custom((_ decoder: Decoder) throws -> Date)
}

/// The strategy to use for decoding `Data` values.
public enum DataDecodingStrategy {
/// Defer to `Data` for decoding.
case deferredToData

/// Decode the `Data` from a Base64-encoded string. This is the default strategy.
case base64

/// Decodes the `Data` as a custom value decoded by the given closure.
case custom((_ decoder: Decoder) throws -> Data)
}
}

// MARK: -
Expand Down
34 changes: 34 additions & 0 deletions Sources/Coders/SwiftEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ extension JSON {
/// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
public var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate

/// The strategy to use in encoding binary data. Defaults to `.base64`.
public var dataEncodingStrategy: DataEncodingStrategy = .base64

/// Creates a new, reusable JSON encoder.
public init() {}

Expand Down Expand Up @@ -252,6 +255,7 @@ extension JSON {
data.keyEncodingStrategy = keyEncodingStrategy
data.applyKeyEncodingStrategyToJSONObject = applyKeyEncodingStrategyToJSONObject
data.dateEncodingStrategy = dateEncodingStrategy
data.dataEncodingStrategy = dataEncodingStrategy
let encoder = _JSONEncoder(data: data)
try encoder.encode(value)
guard let json = encoder.json else {
Expand All @@ -268,6 +272,7 @@ private class EncoderData {
var keyEncodingStrategy: JSON.Encoder.KeyEncodingStrategy = .useDefaultKeys
var applyKeyEncodingStrategyToJSONObject = false
var dateEncodingStrategy: JSON.Encoder.DateEncodingStrategy = .deferredToDate
var dataEncodingStrategy: JSON.Encoder.DataEncodingStrategy = .base64

var shouldRekeyJSONObjects: Bool {
switch keyEncodingStrategy {
Expand All @@ -283,6 +288,7 @@ private class EncoderData {
result.keyEncodingStrategy = keyEncodingStrategy
result.applyKeyEncodingStrategyToJSONObject = applyKeyEncodingStrategyToJSONObject
result.dateEncodingStrategy = dateEncodingStrategy
result.dataEncodingStrategy = dataEncodingStrategy
return result
}
}
Expand Down Expand Up @@ -529,6 +535,19 @@ extension _JSONEncoder: SingleValueEncodingContainer {
self.json = .unboxed([:])
}
}
case let data as Data:
switch _data.dataEncodingStrategy {
case .deferredToData:
try value.encode(to: self)
case .base64:
try encode(data.base64EncodedString())
case .custom(let f):
try f(data, self)
if self.value?.isEmpty ?? true {
// the function didn't encode anything
self.json = .unboxed([:])
}
}
default:
try value.encode(to: self)
}
Expand Down Expand Up @@ -953,4 +972,19 @@ extension JSON.Encoder {
/// an empty object in its place.
case custom((Date, Encoder) throws -> Void)
}

/// The strategy to use for encoding `Data` values.
public enum DataEncodingStrategy {
/// Defer to `Data` for choosing an encoding.
case deferredToData

/// Encode the `Data` as a Base64-encoded string. This is the default strategy.
case base64

/// Encode the `Data` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode
/// an empty object in its place.
case custom((_ data: Data, _ encoder: Encoder) throws -> Void)
}
}
32 changes: 32 additions & 0 deletions Tests/PMJSONTests/SwiftDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,38 @@ final class SwiftDecoderTests: XCTestCase {
let date = try decoder.decode(Date.self, from: 541317080)
XCTAssertEqual(date, Date(timeIntervalSinceReferenceDate: 541318080))
}

// MARK: - DataDecodingStrategy

func testDecodeDataDefaultStrategy() throws {
let input = "hello".data(using: .utf8)!
let decoder = JSON.Decoder()
// don't assume the default format since that's up to Data, just encode it first
let json = try JSON.Encoder().encodeAsJSON(input)
let data = try decoder.decode(Data.self, from: json)
XCTAssertEqual(data, input)
}

func testDecodeDataBase64() throws {
var decoder = JSON.Decoder()
decoder.dataDecodingStrategy = .base64
let data = try decoder.decode(Data.self, from: "aGVsbG8=" as JSON)
XCTAssertEqual(data, "hello".data(using: .utf8)!)
}

func testDecodeDataCustom() throws {
var decoder = JSON.Decoder()
decoder.dataDecodingStrategy = .custom({ (decoder) -> Data in
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
guard let data = Data(base64Encoded: String(str.reversed())) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Could not decode Data.")
}
return data
})
let data = try decoder.decode(Data.self, from: "=8GbsVGa" as JSON)
XCTAssertEqual(data, "hello".data(using: .utf8)!)
}
}

private extension Date {
Expand Down
37 changes: 37 additions & 0 deletions Tests/PMJSONTests/SwiftEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,43 @@ final class SwiftEncoderTests: XCTestCase {
let json = try encoder.encodeAsJSON(Date())
XCTAssertEqual(json, [:])
}

// MARK: - DataEncodingStrategy

func testEncodeDataDefaultStrategy() throws {
let input = "hello".data(using: .utf8)!
let encoder = JSON.Encoder()
// don't assume the default format since that's up to Data, just decode it after
let json = try encoder.encodeAsJSON(input)
let data = try JSON.Decoder().decode(Data.self, from: json)
XCTAssertEqual(data, input)
}

func testEncodeDataBase64() throws {
var encoder = JSON.Encoder()
encoder.dataEncodingStrategy = .base64
let json = try encoder.encodeAsJSON("hello".data(using: .utf8)!)
XCTAssertEqual(json, "aGVsbG8=")
}

func testEncodeDataCustom() throws {
var encoder = JSON.Encoder()
encoder.dataEncodingStrategy = .custom({ (data, encoder) in
let str = String(data.base64EncodedString().reversed())
try str.encode(to: encoder)
})
let json = try encoder.encodeAsJSON("hello".data(using: .utf8)!)
XCTAssertEqual(json, "=8GbsVGa")
}

func testEncodeDataCustomNoEncode() throws {
var encoder = JSON.Encoder()
encoder.dataEncodingStrategy = .custom({ (data, encoder) in
// do nothing
})
let json = try encoder.encodeAsJSON("hello".data(using: .utf8)!)
XCTAssertEqual(json, [:])
}
}

private extension Date {
Expand Down

0 comments on commit 65ef35b

Please sign in to comment.