diff --git a/Blockchain/Sources/Blockchain/Types/JudgementsState.swift b/Blockchain/Sources/Blockchain/Types/JudgementsState.swift index 1f078f6a..68b9a71d 100644 --- a/Blockchain/Sources/Blockchain/Types/JudgementsState.swift +++ b/Blockchain/Sources/Blockchain/Types/JudgementsState.swift @@ -1,18 +1,18 @@ +import Codec import Utils -// TODO: figure out how to deal with orders for items in Set public struct JudgementsState: Sendable, Equatable, Codable { // ψg: Work-reports judged to be correct - public var goodSet: Set + @CodingAs> public var goodSet: Set // ψb: Work-reports judged to be incorrect - public var banSet: Set + @CodingAs> public var banSet: Set // ψw: Work-reports whose validity is judged to be unknowable - public var wonkySet: Set + @CodingAs> public var wonkySet: Set // ψo: Validators who made a judgement found to be incorrect - public var punishSet: Set + @CodingAs> public var punishSet: Set public init( goodSet: Set, diff --git a/Codec/Sources/Codec/CodingAs.swift b/Codec/Sources/Codec/CodingAs.swift new file mode 100644 index 00000000..f85facdd --- /dev/null +++ b/Codec/Sources/Codec/CodingAs.swift @@ -0,0 +1,29 @@ +public protocol CodableAlias: Codable { + associatedtype Alias: Codable + + init(alias: Alias) + var alias: Alias { get } +} + +@propertyWrapper +public struct CodingAs: Codable { + public var wrappedValue: T.Alias + + public init(wrappedValue: T.Alias) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try container.decode(T.self).alias + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(T(alias: wrappedValue)) + } +} + +extension CodingAs: Sendable where T: Sendable, T.Alias: Sendable {} + +extension CodingAs: Equatable where T: Equatable, T.Alias: Equatable {} diff --git a/Codec/Sources/Codec/JamDecoder.swift b/Codec/Sources/Codec/JamDecoder.swift index 0afd76bd..95c82319 100644 --- a/Codec/Sources/Codec/JamDecoder.swift +++ b/Codec/Sources/Codec/JamDecoder.swift @@ -17,10 +17,22 @@ public class JamDecoder { return res } - public static func decode(_ type: T.Type, from data: Data, withConfig config: some Any) throws -> T { - let context = DecodeContext(data: data) - context.userInfo[.config] = config - return try context.decode(type, key: nil) + public static func decode(_ type: T.Type, from data: Data, withConfig config: Any? = nil) throws -> T { + let decoder = JamDecoder(data: data, config: config) + let val = try decoder.decode(type) + try decoder.finalize() + return val + } + + public func finalize() throws { + guard data.isEmpty else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: [], + debugDescription: "Not all data was consumed" + ) + ) + } } } diff --git a/Codec/Sources/Codec/SortedSet.swift b/Codec/Sources/Codec/SortedSet.swift new file mode 100644 index 00000000..9aa071b7 --- /dev/null +++ b/Codec/Sources/Codec/SortedSet.swift @@ -0,0 +1,43 @@ +import Foundation + +public struct SortedSet: Codable, CodableAlias { + public typealias Alias = Set + + public var alias: Alias + + public init(alias: Alias) { + self.alias = alias + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let array = try container.decode([T].self) + + // ensure array is sorted and unique + var previous: T? + for item in array { + guard previous == nil || item > previous! else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Array is not sorted" + ) + ) + } + previous = item + } + + alias = Set(array) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + var array = Array(alias) + array.sort() + try container.encode(array) + } +} + +extension SortedSet: Sendable where T: Sendable, Alias: Sendable {} + +extension SortedSet: Equatable where T: Equatable, Alias: Equatable {} diff --git a/Codec/Tests/CodecTests/CodingAsTests.swift b/Codec/Tests/CodecTests/CodingAsTests.swift new file mode 100644 index 00000000..9e2fe795 --- /dev/null +++ b/Codec/Tests/CodecTests/CodingAsTests.swift @@ -0,0 +1,33 @@ +import Foundation +import Testing + +@testable import Codec + +struct UInt8Alias: CodableAlias, Codable { + typealias T = UInt8 + + var extraByte: UInt8 = 0xAB + var value: UInt8 + + init(alias: UInt8) { + value = alias + } + + var alias: UInt8 { + value + } +} + +struct TestCodable: Codable { + @CodingAs var value: UInt8 +} + +struct CodingAsTests { + @Test func testCodingAs() throws { + let testCase = TestCodable(value: 0x23) + let encoded = try JamEncoder.encode(testCase) + #expect(encoded == Data([0xAB, 0x23])) + let decoded = try JamDecoder.decode(TestCodable.self, from: encoded, withConfig: testCase) + #expect(decoded.value == 0x23) + } +} diff --git a/Codec/Tests/CodecTests/SortedSetTests.swift b/Codec/Tests/CodecTests/SortedSetTests.swift new file mode 100644 index 00000000..d0eb7167 --- /dev/null +++ b/Codec/Tests/CodecTests/SortedSetTests.swift @@ -0,0 +1,27 @@ +import Foundation +import Testing + +@testable import Codec + +struct SortedSetTests { + @Test func encode() throws { + let testCase: SortedSet = SortedSet(alias: [1, 2, 3]) + let encoded = try JamEncoder.encode(testCase) + #expect(encoded == Data([3, 1, 2, 3])) + } + + @Test func decode() throws { + let decoded = try JamDecoder.decode(SortedSet.self, from: Data([12, 1, 2, 3])) + #expect(decoded.alias == [1, 2, 3]) + } + + @Test func invalidData() throws { + #expect(throws: DecodingError.self) { + try JamDecoder.decode(SortedSet.self, from: Data([12, 1, 2, 2])) + } + + #expect(throws: DecodingError.self) { + try JamDecoder.decode(SortedSet.self, from: Data([12, 3, 2, 1])) + } + } +}