Skip to content

Commit 2ac2b44

Browse files
authored
Implement Append and Prepend Support (#27)
1 parent 526c67d commit 2ac2b44

File tree

7 files changed

+214
-38
lines changed

7 files changed

+214
-38
lines changed

Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift

+13
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ extension ByteBuffer {
8686
}
8787
}
8888
}
89+
90+
if let storageMode = flags.storageMode {
91+
switch storageMode {
92+
case .append:
93+
self.writeInteger(UInt8.whitespace)
94+
self.writeInteger(UInt8.M)
95+
self.writeInteger(UInt8.A)
96+
case .prepend:
97+
self.writeInteger(UInt8.whitespace)
98+
self.writeInteger(UInt8.M)
99+
self.writeInteger(UInt8.P)
100+
}
101+
}
89102
}
90103
}
91104

Sources/SwiftMemcache/Extensions/UInt8+Characters.swift

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ extension UInt8 {
2222
static var d: UInt8 = .init(ascii: "d")
2323
static var v: UInt8 = .init(ascii: "v")
2424
static var T: UInt8 = .init(ascii: "T")
25+
static var M: UInt8 = .init(ascii: "M")
26+
static var P: UInt8 = .init(ascii: "P")
27+
static var A: UInt8 = .init(ascii: "A")
2528
static var zero: UInt8 = .init(ascii: "0")
2629
static var nine: UInt8 = .init(ascii: "9")
2730
}

Sources/SwiftMemcache/MemcachedConnection.swift

+60
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,64 @@ public actor MemcachedConnection {
280280
throw MemcachedConnectionError.connectionShutdown
281281
}
282282
}
283+
284+
// MARK: - Prepending a Value
285+
286+
/// Prepend a value to an existing key in the Memcache server.
287+
///
288+
/// - Parameters:
289+
/// - key: The key to prepend the value to.
290+
/// - value: The `MemcachedValue` to prepend.
291+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
292+
public func prepend(_ key: String, value: some MemcachedValue) async throws {
293+
switch self.state {
294+
case .initial(_, let bufferAllocator, _, _),
295+
.running(let bufferAllocator, _, _, _):
296+
297+
var buffer = bufferAllocator.buffer(capacity: 0)
298+
value.writeToBuffer(&buffer)
299+
var flags: MemcachedFlags
300+
301+
flags = MemcachedFlags()
302+
flags.storageMode = .prepend
303+
304+
let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
305+
let request = MemcachedRequest.set(command)
306+
307+
_ = try await self.sendRequest(request)
308+
309+
case .finished:
310+
throw MemcachedConnectionError.connectionShutdown
311+
}
312+
}
313+
314+
// MARK: - Appending a Value
315+
316+
/// Append a value to an existing key in the Memcache server.
317+
///
318+
/// - Parameters:
319+
/// - key: The key to append the value to.
320+
/// - value: The `MemcachedValue` to append.
321+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
322+
public func append(_ key: String, value: some MemcachedValue) async throws {
323+
switch self.state {
324+
case .initial(_, let bufferAllocator, _, _),
325+
.running(let bufferAllocator, _, _, _):
326+
327+
var buffer = bufferAllocator.buffer(capacity: 0)
328+
value.writeToBuffer(&buffer)
329+
var flags: MemcachedFlags
330+
331+
flags = MemcachedFlags()
332+
flags.storageMode = .append
333+
334+
let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
335+
let request = MemcachedRequest.set(command)
336+
337+
_ = try await self.sendRequest(request)
338+
339+
case .finished:
340+
throw MemcachedConnectionError.connectionShutdown
341+
}
342+
}
283343
}

Sources/SwiftMemcache/MemcachedFlags.swift

+14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ struct MemcachedFlags {
3131
/// If set, the item is considered to be expired after this number of seconds.
3232
var timeToLive: TimeToLive?
3333

34+
/// Mode for the 'ms' (meta set) command (corresponding to the 'M' flag).
35+
///
36+
/// Represents the mode of the 'ms' command, which determines the behavior of the data operation.
37+
/// The default mode is 'set'.
38+
var storageMode: StorageMode?
39+
3440
init() {}
3541
}
3642

@@ -42,4 +48,12 @@ public enum TimeToLive: Equatable, Hashable {
4248
case expiresAt(ContinuousClock.Instant)
4349
}
4450

51+
/// Enum representing the Memcached 'ms' (meta set) command modes (corresponding to the 'M' flag).
52+
public enum StorageMode: Equatable, Hashable {
53+
/// The 'append' command. If the item exists, append the new value to its data.
54+
case append
55+
/// The 'prepend' command. If the item exists, prepend the new value to its data.
56+
case prepend
57+
}
58+
4559
extension MemcachedFlags: Hashable {}

Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift

+52
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,58 @@ final class MemcachedIntegrationTest: XCTestCase {
254254
}
255255
}
256256

257+
func testPrependValue() async throws {
258+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
259+
defer {
260+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
261+
}
262+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
263+
264+
try await withThrowingTaskGroup(of: Void.self) { group in
265+
group.addTask { try await memcachedConnection.run() }
266+
267+
// Set key and initial value
268+
let initialValue = "foo"
269+
try await memcachedConnection.set("greet", value: initialValue)
270+
271+
// Prepend value to key
272+
let prependValue = "Hi"
273+
try await memcachedConnection.prepend("greet", value: prependValue)
274+
275+
// Get value for key after prepend operation
276+
let updatedValue: String? = try await memcachedConnection.get("greet")
277+
XCTAssertEqual(updatedValue, prependValue + initialValue, "Received value should be the same as the concatenation of prependValue and initialValue")
278+
279+
group.cancelAll()
280+
}
281+
}
282+
283+
func testAppendValue() async throws {
284+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
285+
defer {
286+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
287+
}
288+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
289+
290+
try await withThrowingTaskGroup(of: Void.self) { group in
291+
group.addTask { try await memcachedConnection.run() }
292+
293+
// Set key and initial value
294+
let initialValue = "hi"
295+
try await memcachedConnection.set("greet", value: initialValue)
296+
297+
// Append value to key
298+
let appendValue = "foo"
299+
try await memcachedConnection.append("greet", value: appendValue)
300+
301+
// Get value for key after append operation
302+
let updatedValue: String? = try await memcachedConnection.get("greet")
303+
XCTAssertEqual(updatedValue, initialValue + appendValue, "Received value should be the same as the concatenation of initialValue and appendValue")
304+
305+
group.cancelAll()
306+
}
307+
}
308+
257309
func testMemcachedConnectionWithUInt() async throws {
258310
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
259311
defer {

Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift

+20
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,24 @@ final class MemcachedFlagsTests: XCTestCase {
3737
XCTFail("Flag timeToLive is nil")
3838
}
3939
}
40+
41+
func testStorageModeAppend() {
42+
var flags = MemcachedFlags()
43+
flags.storageMode = .append
44+
if case .append? = flags.storageMode {
45+
XCTAssertTrue(true)
46+
} else {
47+
XCTFail("Flag storageMode is not .append")
48+
}
49+
}
50+
51+
func testStorageModePrepend() {
52+
var flags = MemcachedFlags()
53+
flags.storageMode = .prepend
54+
if case .prepend? = flags.storageMode {
55+
XCTAssertTrue(true)
56+
} else {
57+
XCTFail("Flag storageMode is not .prepend")
58+
}
59+
}
4060
}

Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift

+52-38
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ final class MemcachedRequestEncoderTests: XCTestCase {
2424
self.encoder = MemcachedRequestEncoder()
2525
}
2626

27+
func encodeRequest(_ request: MemcachedRequest) -> ByteBuffer {
28+
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
29+
do {
30+
try self.encoder.encode(data: request, out: &outBuffer)
31+
} catch {
32+
XCTFail("Encoding failed with error: \(error)")
33+
}
34+
return outBuffer
35+
}
36+
2737
func testEncodeSetRequest() {
2838
// Prepare a MemcachedRequest
2939
var buffer = ByteBufferAllocator().buffer(capacity: 2)
@@ -32,17 +42,46 @@ final class MemcachedRequestEncoderTests: XCTestCase {
3242
let request = MemcachedRequest.set(command)
3343

3444
// pass our request through the encoder
35-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
36-
do {
37-
try self.encoder.encode(data: request, out: &outBuffer)
38-
} catch {
39-
XCTFail("Encoding failed with error: \(error)")
40-
}
45+
let outBuffer = self.encodeRequest(request)
4146

4247
let expectedEncodedData = "ms foo 2\r\nhi\r\n"
4348
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
4449
}
4550

51+
func testEncodeAppendRequest() {
52+
// Prepare a MemcachedRequest
53+
var buffer = ByteBufferAllocator().buffer(capacity: 2)
54+
buffer.writeString("hi")
55+
56+
var flags = MemcachedFlags()
57+
flags.storageMode = .append
58+
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
59+
let request = MemcachedRequest.set(command)
60+
61+
// pass our request through the encoder
62+
let outBuffer = self.encodeRequest(request)
63+
64+
let expectedEncodedData = "ms foo 2 MA\r\nhi\r\n"
65+
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
66+
}
67+
68+
func testEncodePrependRequest() {
69+
// Prepare a MemcachedRequest
70+
var buffer = ByteBufferAllocator().buffer(capacity: 2)
71+
buffer.writeString("hi")
72+
73+
var flags = MemcachedFlags()
74+
flags.storageMode = .prepend
75+
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
76+
let request = MemcachedRequest.set(command)
77+
78+
// pass our request through the encoder
79+
let outBuffer = self.encodeRequest(request)
80+
81+
let expectedEncodedData = "ms foo 2 MP\r\nhi\r\n"
82+
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
83+
}
84+
4685
func testEncodeTouchRequest() {
4786
// Prepare a MemcachedRequest
4887
var flags = MemcachedFlags()
@@ -53,12 +92,7 @@ final class MemcachedRequestEncoderTests: XCTestCase {
5392
let request = MemcachedRequest.get(command)
5493

5594
// pass our request through the encoder
56-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
57-
do {
58-
try self.encoder.encode(data: request, out: &outBuffer)
59-
} catch {
60-
XCTFail("Encoding failed with error: \(error)")
61-
}
95+
let outBuffer = self.encodeRequest(request)
6296

6397
let expectedEncodedData = "mg foo T89\r\n"
6498
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
@@ -75,12 +109,7 @@ final class MemcachedRequestEncoderTests: XCTestCase {
75109
let request = MemcachedRequest.get(command)
76110

77111
// pass our request through the encoder
78-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
79-
do {
80-
try self.encoder.encode(data: request, out: &outBuffer)
81-
} catch {
82-
XCTFail("Encoding failed with error: \(error)")
83-
}
112+
let outBuffer = self.encodeRequest(request)
84113

85114
// Time-To-Live has been transformed to a Unix timestamp
86115
var timespec = timespec()
@@ -109,12 +138,7 @@ final class MemcachedRequestEncoderTests: XCTestCase {
109138
let request = MemcachedRequest.get(command)
110139

111140
// pass our request through the encoder
112-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
113-
do {
114-
try self.encoder.encode(data: request, out: &outBuffer)
115-
} catch {
116-
XCTFail("Encoding failed with error: \(error)")
117-
}
141+
let outBuffer = self.encodeRequest(request)
118142

119143
let expectedEncodedData = "mg foo T0\r\n"
120144
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
@@ -128,13 +152,8 @@ final class MemcachedRequestEncoderTests: XCTestCase {
128152

129153
let request = MemcachedRequest.get(command)
130154

131-
// Pass our request through the encoder
132-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
133-
do {
134-
try self.encoder.encode(data: request, out: &outBuffer)
135-
} catch {
136-
XCTFail("Encoding failed with error: \(error)")
137-
}
155+
// pass our request through the encoder
156+
let outBuffer = self.encodeRequest(request)
138157

139158
let expectedEncodedData = "mg foo v\r\n"
140159
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
@@ -145,13 +164,8 @@ final class MemcachedRequestEncoderTests: XCTestCase {
145164
let command = MemcachedRequest.DeleteCommand(key: "foo")
146165
let request = MemcachedRequest.delete(command)
147166

148-
// Pass our request through the encoder
149-
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
150-
do {
151-
try self.encoder.encode(data: request, out: &outBuffer)
152-
} catch {
153-
XCTFail("Encoding failed with error: \(error)")
154-
}
167+
// pass our request through the encoder
168+
let outBuffer = self.encodeRequest(request)
155169

156170
let expectedEncodedData = "md foo\r\n"
157171
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)

0 commit comments

Comments
 (0)