Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions Sources/Valkey/Commands/Custom/GeoCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension GEOSEARCH {
case .array(let array):
var arrayIterator = array.makeIterator()
guard let member = arrayIterator.next() else {
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.invalidArraySize(array)
}
self.member = try String(fromRESP: member)
self.attributes = array.dropFirst().map { $0 }
Expand All @@ -56,7 +56,7 @@ extension GEOSEARCH {
self.attributes = []

default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array, .bulkString], token: token)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Valkey/Commands/Custom/ListCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension LMPOP {
case .array(let array):
(self.key, self.values) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand Down
20 changes: 10 additions & 10 deletions Sources/Valkey/Commands/Custom/ServerCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//
extension ROLE {
public enum Response: RESPTokenDecodable, Sendable {
struct DecodeError: Error {}
struct MissingValueDecodeError: Error {}
public struct Primary: Sendable {
public struct Replica: RESPTokenDecodable, Sendable {
public let ip: String
Expand All @@ -23,7 +23,7 @@ extension ROLE {

init(arrayIterator: inout RESPToken.Array.Iterator) throws {
guard let replicationOffsetToken = arrayIterator.next(), let replicasToken = arrayIterator.next() else {
throw DecodeError()
throw MissingValueDecodeError()
}
self.replicationOffset = try .init(fromRESP: replicationOffsetToken)
self.replicas = try .init(fromRESP: replicasToken)
Expand All @@ -39,7 +39,7 @@ extension ROLE {
public init(fromRESP token: RESPToken) throws {
let string = try String(fromRESP: token)
guard let state = State(rawValue: string) else {
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError(.unexpectedToken, token: token)
}
self = state
}
Expand All @@ -55,7 +55,7 @@ extension ROLE {
let stateToken = arrayIterator.next(),
let replicationToken = arrayIterator.next()
else {
throw DecodeError()
throw MissingValueDecodeError()
}
self.primaryIP = try .init(fromRESP: primaryIPToken)
self.primaryPort = try .init(fromRESP: primaryPortToken)
Expand All @@ -67,7 +67,7 @@ extension ROLE {
public let primaryNames: [String]

init(arrayIterator: inout RESPToken.Array.Iterator) throws {
guard let primaryNamesToken = arrayIterator.next() else { throw DecodeError() }
guard let primaryNamesToken = arrayIterator.next() else { throw MissingValueDecodeError() }
self.primaryNames = try .init(fromRESP: primaryNamesToken)
}
}
Expand All @@ -81,7 +81,7 @@ extension ROLE {
do {
var iterator = array.makeIterator()
guard let roleToken = iterator.next() else {
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.invalidArraySize(array)
}
let role = try String(fromRESP: roleToken)
switch role {
Expand All @@ -95,13 +95,13 @@ extension ROLE {
let sentinel = try Sentinel(arrayIterator: &iterator)
self = .sentinel(sentinel)
default:
throw DecodeError()
throw RESPDecodeError(.unexpectedToken, token: token)
}
} catch {
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
} catch is MissingValueDecodeError {
throw RESPDecodeError.invalidArraySize(array)
}
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/Valkey/Commands/Custom/SetCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ extension SSCAN {

public init(fromRESP token: RESPToken) throws {
// cursor is encoded as a bulkString, but should be
let (cursorString, elements) = try token.decodeArrayElements(as: (String, RESPToken.Array).self)
guard let cursor = Int(cursorString) else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
let (cursor, elements) = try token.decodeArrayElements(as: (Int, RESPToken.Array).self)
self.cursor = cursor
self.elements = elements
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Valkey/Commands/Custom/SortedSetCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct SortedSetEntry: RESPTokenDecodable, Sendable {
case .array(let array):
(self.value, self.score) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand Down Expand Up @@ -56,7 +56,7 @@ extension ZMPOP {
case .array(let array):
(self.key, self.values) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions Sources/Valkey/Commands/Custom/StreamCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public struct XREADMessage: RESPTokenDecodable, Sendable {
self.id = id
self.fields = keyValuePairs
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}

Expand Down Expand Up @@ -77,7 +77,7 @@ public struct XREADGroupMessage: RESPTokenDecodable, Sendable {
self.id = id
self.fields = keyValuePairs
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -100,7 +100,7 @@ public struct XREADStreams<Message>: RESPTokenDecodable, Sendable where Message:
return Stream(key: key, messages: messages)
}
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.map], token: token)
}
}
}
Expand All @@ -116,7 +116,7 @@ public struct XAUTOCLAIMResponse: RESPTokenDecodable, Sendable {
case .array(let array):
(self.streamID, self.messages, self.deletedMessages) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -143,7 +143,7 @@ public enum XCLAIMResponse: RESPTokenDecodable, Sendable {
self = try .ids(array.decode())
}
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -164,7 +164,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
case .array(let array):
(self.consumer, self.count) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -178,7 +178,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
case .array(let array):
(self.pendingMessageCount, self.minimumID, self.maximumID, self.consumers) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -194,7 +194,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
case .array(let array):
(self.id, self.consumer, self.millisecondsSinceDelivered, self.numberOfTimesDelivered) = try array.decodeElements()
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand All @@ -205,7 +205,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
case .array(let array):
self.messages = try array.decode(as: [PendingMessage].self)
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/Valkey/Commands/Custom/StringCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ extension LCS {
default: break
}
}
guard let matches else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
guard let length else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
guard let matches else { throw RESPDecodeError.missingToken(key: "matches", token: token) }
guard let length else { throw RESPDecodeError.missingToken(key: "length", token: token) }
self = .matches(length: numericCast(length), matches: matches)
default:
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
throw RESPDecodeError.tokenMismatch(expected: [.bulkString, .integer, .map], token: token)

}
}
}
Expand Down
81 changes: 81 additions & 0 deletions Sources/Valkey/RESP/RESPDecodeError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// This source file is part of the valkey-swift project
// Copyright (c) 2025 the valkey-swift project authors
//
// See LICENSE.txt for license information
// SPDX-License-Identifier: Apache-2.0
//
/// Error returned when decoding a RESPToken.
/// Error thrown when decoding RESPTokens
public struct RESPDecodeError: Error {
/// Error code for decode error
public struct ErrorCode: Sendable, Equatable, CustomStringConvertible {
fileprivate enum Code: Sendable, Equatable {
case tokenMismatch
case invalidArraySize
case missingToken
case cannotParseInteger
case cannotParseDouble
case unexpectedToken
}

fileprivate let code: Code
fileprivate init(_ code: Code) {
self.code = code
}

public var description: String { String(describing: self.code) }

/// Token does not match one of the expected tokens
public static var tokenMismatch: Self { .init(.tokenMismatch) }
/// Does not match the expected array size
public static var invalidArraySize: Self { .init(.invalidArraySize) }
/// Token is missing
public static var missingToken: Self { .init(.missingToken) }
/// Failed to parse an integer
public static var cannotParseInteger: Self { .init(.cannotParseInteger) }
/// Failed to parse a double
public static var cannotParseDouble: Self { .init(.cannotParseDouble) }
/// Token is not as expected
public static var unexpectedToken: Self { .init(.unexpectedToken) }
}
public let errorCode: ErrorCode
public let message: String?
public let token: RESPToken.Value

public init(_ errorCode: ErrorCode, token: RESPToken.Value, message: String? = nil) {
self.errorCode = errorCode
self.token = token
self.message = message
}

public init(_ errorCode: ErrorCode, token: RESPToken, message: String? = nil) {
self = .init(errorCode, token: token.value, message: message)
}

/// Token does not match one of the expected tokens
public static func tokenMismatch(expected: [RESPTypeIdentifier], token: RESPToken) -> Self {
if expected.count == 0 {
return .init(.tokenMismatch, token: token, message: "Found unexpected token while decoding")
} else if expected.count == 1 {
return .init(.tokenMismatch, token: token, message: "Expected to find a \(expected[0])")
} else {
let expectedTokens = "\(expected.dropLast().map { "\($0)" }.joined(separator: ", ")) or \(expected.last!)"
return .init(.tokenMismatch, token: token, message: "Expected to find a \(expectedTokens) token")
}
}
/// Does not match the expected array size
public static func invalidArraySize(_ array: RESPToken.Array) -> Self {
.init(.invalidArraySize, token: .array(array))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could be worth adding a message just like tokenMismatch() to make this error more descriptive. For example for empty array, the message can say "Empty Array" and so on.

}
/// Token associated with key is missing
public static func missingToken(key: String, token: RESPToken) -> Self {
.init(.missingToken, token: token, message: "Expected map to contain token with key \"\(key)\"")
}
}

extension RESPDecodeError: CustomStringConvertible {
public var description: String {
"Error: \"\(self.message ?? String(describing: self.errorCode))\", token: \(self.token.debugDescription)"
}
}
Loading
Loading