Skip to content
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
39 changes: 28 additions & 11 deletions Sources/MultiArray/Extensions/Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,43 @@

extension MultiArray: Decodable where Element: Decodable {
private enum CodingKeys: CodingKey {
case version
case count
case values
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let count = try container.decode(Int.self, forKey: .count)

var values = try container.nestedUnkeyedContainer(forKey: .values)
self = try .init(count: count) { _ in
try values.decode(Element.self)
}
// If the version tag is absent, decode as v1. Only the 2.0.0 release
// did not include a version tag, but was added since 2.1.0. This format
// is a fairly (human) readable encoding of just {count, values}.
let version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1

switch version {
case 1:
let count = try container.decode(Int.self, forKey: .count)
var values = try container.nestedUnkeyedContainer(forKey: .values)
self = try .init(count: count) { _ in
try values.decode(Element.self)
}

if !values.isAtEnd {
throw DecodingError.dataCorrupted(
.init(
codingPath: values.codingPath,
debugDescription: "More values than expected for count: \(count)"
)
)
}

if !values.isAtEnd {
throw DecodingError.dataCorrupted(
.init(
codingPath: values.codingPath,
debugDescription: "More values than expected for count: \(count)"
default:
throw DecodingError.dataCorrupted(
.init(
codingPath: container.codingPath,
debugDescription: "Unsupported MultiArray coding version: \(version)"
)
)
)
}
}
}
6 changes: 6 additions & 0 deletions Sources/MultiArray/Extensions/Encodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@

extension MultiArray: Encodable where Element: Encodable {
private enum CodingKeys: CodingKey {
case version
case count
case values
}

// Current version of the encoding schema.
// This needs to be bumped whenever the format changes.
private static var codingVersion: Int { 1 }

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(Self.codingVersion, forKey: .version)
try container.encode(self.count, forKey: .count)

var values = container.nestedUnkeyedContainer(forKey: .values)
Expand Down
36 changes: 36 additions & 0 deletions Tests/MultiArrayTests/MultiArrayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,32 @@ struct MultiArrayTests {
let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any]

// Keys exist
#expect(jsonObject?["version"] != nil)
#expect(jsonObject?["count"] != nil)
#expect(jsonObject?["values"] != nil)

// Values have expected contents
#expect(jsonObject?["version"] as? Int == 1)
#expect(jsonObject?["count"] as? Int == 3)
#expect(jsonObject?["values"] as? [Int] == [10, 20, 30])
}

@Test
func decodeWithoutVersion() throws {
let json = """
{
"count": 3,
"values": [1, 2, 3]
}
"""

let data = Data(json.utf8)
let decoder = JSONDecoder()

let decoded = try decoder.decode(MultiArray<Int>.self, from: data)
#expect(decoded == MultiArray<Int>([1, 2, 3]))
}

@Test
func failNotEnoughValues() throws {
let json = """
Expand Down Expand Up @@ -158,6 +176,24 @@ struct MultiArrayTests {
}
}

@Test
func failUnsupportedVersion() throws {
let json = """
{
"version": 999,
"count": 3,
"values": [1, 2, 3]
}
"""

let data = Data(json.utf8)
let decoder = JSONDecoder()

#expect(throws: DecodingError.self) {
_ = try decoder.decode(MultiArray<Int>.self, from: data)
}
}

@Suite
struct RoundTripTests {
@Suite
Expand Down