Skip to content

Commit e964ba0

Browse files
committed
Merge branch 'cleanup-unsafe' into '47-proposal-feedback'
Revisit `RESPValue` and `RESPValueConvertible` implementations. See merge request Mordil/swift-redis-nio-client!67
2 parents f445822 + cd9bd04 commit e964ba0

File tree

9 files changed

+203
-181
lines changed

9 files changed

+203
-181
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let package = Package(
2727
],
2828
targets: [
2929
.target(name: "RedisNIO", dependencies: ["NIO", "Logging", "Metrics"]),
30-
.testTarget(name: "RedisNIOTests", dependencies: ["RedisNIO", "NIO"])
30+
.target(name: "RedisNIOTestUtils", dependencies: ["NIO", "RedisNIO"]),
31+
.testTarget(name: "RedisNIOTests", dependencies: ["RedisNIO", "NIO", "RedisNIOTestUtils"])
3132
]
3233
)

Sources/RedisNIO/RESP/RESPTranslator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ extension RESPTranslator {
224224
guard elementCount > -1 else { return .null } // '*-1\r\n'
225225
guard elementCount > 0 else { return .array([]) } // '*0\r\n'
226226

227-
var results: ContiguousArray<RESPValue> = []
227+
var results: [RESPValue] = []
228228
results.reserveCapacity(elementCount)
229229

230230
for _ in 0..<elementCount {

Sources/RedisNIO/RESP/RESPValue.swift

Lines changed: 67 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -15,190 +15,117 @@
1515
import struct Foundation.Data
1616
import NIO
1717

18-
extension String {
19-
@inline(__always)
20-
var byteBuffer: ByteBuffer {
21-
var buffer = RESPValue.allocator.buffer(capacity: self.count)
22-
buffer.writeString(self)
23-
return buffer
24-
}
25-
}
26-
27-
extension Data {
28-
@inline(__always)
29-
var byteBuffer: ByteBuffer {
30-
var buffer = RESPValue.allocator.buffer(capacity: self.count)
31-
buffer.writeBytes(self)
32-
return buffer
33-
}
34-
}
35-
3618
/// A representation of a Redis Serialization Protocol (RESP) primitive value.
3719
///
20+
/// This enum representation should be used only as a temporary intermediate representation of values, and should be sent to a Redis server or converted to Swift
21+
/// types as soon as possible.
22+
///
23+
/// Redis servers expect a single message packed into an `.array`, with all elements being `.bulkString` representations of values. As such, all initializers
24+
/// convert to `.bulkString` representations, as well as default conformances for `RESPValueConvertible`.
25+
///
26+
/// Each case of this type is a different listing in the RESP specification, and several computed properties are available to consistently convert values into Swift types.
27+
///
3828
/// See: [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
3929
public enum RESPValue {
4030
case null
4131
case simpleString(ByteBuffer)
4232
case bulkString(ByteBuffer?)
4333
case error(RedisError)
4434
case integer(Int)
45-
case array(ContiguousArray<RESPValue>)
35+
case array([RESPValue])
4636

47-
fileprivate static let allocator = ByteBufferAllocator()
37+
/// A `NIO.ByteBufferAllocator` for use in creating `.simpleString` and `.bulkString` representations directly, if needed.
38+
static let allocator = ByteBufferAllocator()
4839

49-
/// Initializes a `bulkString` by converting the provided string input.
50-
public init(bulk value: String? = nil) {
51-
self = .bulkString(value?.byteBuffer)
40+
/// Initializes a `bulkString` value.
41+
/// - Parameter value: The `String` to store in a `.bulkString` representation.
42+
public init(bulk value: String) {
43+
var buffer = RESPValue.allocator.buffer(capacity: value.count)
44+
buffer.writeString(value)
45+
self = .bulkString(buffer)
5246
}
5347

48+
/// Initializes a `bulkString` value.
49+
/// - Parameter value: The `Int` value to store in a `.bulkString` representation.
5450
public init(bulk value: Int) {
55-
self = .bulkString(value.description.byteBuffer)
56-
}
57-
58-
public init(_ source: RESPValueConvertible) {
59-
self = source.convertedToRESPValue()
60-
}
61-
}
62-
63-
// MARK: Expressible by Literals
64-
65-
extension RESPValue: ExpressibleByStringLiteral {
66-
/// Initializes a bulk string from a String literal
67-
public init(stringLiteral value: String) {
68-
self = .bulkString(value.byteBuffer)
69-
}
70-
}
71-
72-
extension RESPValue: ExpressibleByArrayLiteral {
73-
/// Initializes an array from an Array literal
74-
public init(arrayLiteral elements: RESPValue...) {
75-
self = .array(.init(elements))
51+
self.init(bulk: value.description)
7652
}
77-
}
7853

79-
extension RESPValue: ExpressibleByNilLiteral {
80-
/// Initializes null from a nil literal
81-
public init(nilLiteral: ()) {
82-
self = .null
83-
}
84-
}
85-
86-
extension RESPValue: ExpressibleByIntegerLiteral {
87-
/// Initializes an integer from an integer literal
88-
public init(integerLiteral value: Int) {
89-
self = .integer(value)
54+
/// Stores the representation determined by the `RESPValueConvertible` value.
55+
/// - Important: If you are sending this value to a Redis server, the type should be convertible to a `.bulkString`.
56+
/// - Parameter value: The value that needs to be converted and stored in `RESPValue` format.
57+
public init<Value: RESPValueConvertible>(_ value: Value) {
58+
self = value.convertedToRESPValue()
9059
}
9160
}
9261

9362
// MARK: Custom String Convertible
9463

9564
extension RESPValue: CustomStringConvertible {
65+
/// See `CustomStringConvertible.description`
9666
public var description: String {
9767
switch self {
98-
case .integer, .simpleString, .bulkString: return self.string!
68+
case let .simpleString(buffer),
69+
let .bulkString(.some(buffer)):
70+
guard let value = String(fromRESP: self) else { return "\(buffer)" } // default to ByteBuffer's representation
71+
return value
72+
73+
// .integer, .error, and .bulkString(.none) conversions to String always succeed
74+
case .integer,
75+
.bulkString(.none):
76+
return String(fromRESP: self)!
77+
9978
case .null: return "NULL"
100-
case let .array(elements): return "[\(elements.map({ $0.description }).joined(separator: ","))]"
10179
case let .error(e): return e.message
80+
case let .array(elements): return "[\(elements.map({ $0.description }).joined(separator: ","))]"
10281
}
10382
}
10483
}
10584

106-
// MARK: Computed Values
85+
// MARK: Unwrapped Values
10786

10887
extension RESPValue {
109-
/// The `ByteBuffer` storage for either `.simpleString` or `.bulkString` representations.
110-
public var byteBuffer: ByteBuffer? {
111-
switch self {
112-
case let .simpleString(buffer),
113-
let .bulkString(.some(buffer)): return buffer
114-
default: return nil
115-
}
116-
}
117-
118-
/// The storage value for `array` representations.
119-
public var array: ContiguousArray<RESPValue>? {
120-
guard case .array(let array) = self else { return nil }
121-
return array
122-
}
88+
/// The unwrapped value for `.array` representations.
89+
/// - Note: This is a shorthand for `Array<RESPValue>.init(fromRESP:)`
90+
public var array: [RESPValue]? { return [RESPValue](fromRESP: self) }
12391

124-
/// The storage value for `integer` representations.
125-
public var int: Int? {
126-
switch self {
127-
case let .integer(value): return value
128-
default: return nil
129-
}
130-
}
92+
/// The unwrapped value as an `Int`.
93+
/// - Note: This is a shorthand for `Int(fromRESP:)`.
94+
public var int: Int? { return Int(fromRESP: self) }
13195

132-
/// Returns `true` if the value represents a `null` value from Redis.
96+
/// Returns `true` if the unwrapped value is `.null`.
13397
public var isNull: Bool {
134-
switch self {
135-
case .null: return true
136-
default: return false
137-
}
138-
}
139-
140-
/// The error returned from Redis.
141-
public var error: RedisError? {
142-
switch self {
143-
case .error(let error): return error
144-
default: return nil
145-
}
98+
guard case .null = self else { return false }
99+
return true
146100
}
147-
}
148-
149-
// MARK: Conversion Values
150-
151-
extension RESPValue {
152-
/// The `RESPValue` converted to a `String`.
153-
/// - Important: This will always return `nil` from `.error`, `.null`, and `array` cases.
154-
/// - Note: This creates a `String` using UTF-8 encoding.
155-
public var string: String? {
156-
switch self {
157-
case let .integer(value): return value.description
158-
case let .simpleString(buffer),
159-
let .bulkString(.some(buffer)):
160-
return buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes)
161101

162-
case .bulkString(.none): return ""
163-
default: return nil
164-
}
165-
}
102+
/// The unwrapped `RedisError` that was returned from Redis.
103+
/// - Note: This is a shorthand for `RedisError(fromRESP:)`.
104+
public var error: RedisError? { return RedisError(fromRESP: self) }
166105

167-
/// The raw bytes of the `RESPValue` representation.
168-
/// - Important: This will always return `nil` from `.error` and `.null` cases.
169-
public var bytes: [UInt8]? {
106+
/// The unwrapped `NIO.ByteBuffer` for `.simpleString` or `.bulkString` representations.
107+
public var byteBuffer: ByteBuffer? {
170108
switch self {
171-
case let .integer(value): return withUnsafeBytes(of: value, RESPValue.copyMemory)
172-
case let .array(values): return values.withUnsafeBytes(RESPValue.copyMemory)
173109
case let .simpleString(buffer),
174-
let .bulkString(.some(buffer)):
175-
return buffer.getBytes(at: buffer.readerIndex, length: buffer.readableBytes)
110+
let .bulkString(.some(buffer)): return buffer
176111

177-
case .bulkString(.none): return []
178112
default: return nil
179113
}
180114
}
115+
}
181116

182-
public var data: Data? {
183-
switch self {
184-
case let .integer(value): return withUnsafeBytes(of: value, RESPValue.copyMemory)
185-
case let .array(values): return values.withUnsafeBytes(RESPValue.copyMemory)
186-
case let .simpleString(buffer),
187-
let .bulkString(.some(buffer)):
188-
return buffer.withUnsafeReadableBytes(RESPValue.copyMemory)
189-
190-
case .bulkString(.none): return Data()
191-
default: return nil
192-
}
193-
}
117+
// MARK: Conversion Values
194118

195-
// SR-9604
196-
@inline(__always)
197-
private static func copyMemory(_ ptr: UnsafeRawBufferPointer) -> Data {
198-
return Data(UnsafeRawBufferPointer(ptr).bindMemory(to: UInt8.self))
199-
}
200-
@inline(__always)
201-
private static func copyMemory(_ ptr: UnsafeRawBufferPointer) -> [UInt8]? {
202-
return Array<UInt8>(UnsafeRawBufferPointer(ptr).bindMemory(to: UInt8.self))
203-
}
119+
extension RESPValue {
120+
/// The value as a UTF-8 `String` representation.
121+
/// - Note: This is a shorthand for `String.init(fromRESP:)`.
122+
public var string: String? { return String(fromRESP: self) }
123+
124+
/// The data stored in either a `.simpleString` or `.bulkString` represented as `Foundation.Data` instead of `NIO.ByteBuffer`.
125+
/// - Note: This is a shorthand for `Data.init(fromRESP:)`.
126+
public var data: Data? { return Data(fromRESP: self) }
127+
128+
/// The raw bytes stored in the `.simpleString` or `.bulkString` representations.
129+
/// - Note: This is a shorthand for `Array<UInt8>.init(fromRESP:)`.
130+
public var bytes: [UInt8]? { return [UInt8](fromRESP: self) }
204131
}

0 commit comments

Comments
 (0)