Skip to content

Commit 942dcf4

Browse files
authored
Implement an Arithmetic Request supporting increment and decrement operations (#29)
1 parent 9ae24ae commit 942dcf4

9 files changed

+203
-2
lines changed

Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ extension ByteBuffer {
5353
/// - parameters:
5454
/// - flags: An instance of MemcachedFlags that holds the flags intended to be serialized and written to the ByteBuffer.
5555
mutating func writeMemcachedFlags(flags: MemcachedFlags) {
56+
// Ensure that both storageMode and arithmeticMode aren't set at the same time.
57+
precondition(!(flags.storageMode != nil && flags.arithmeticMode != nil), "Cannot specify both a storage and arithmetic mode.")
58+
5659
if let shouldReturnValue = flags.shouldReturnValue, shouldReturnValue {
5760
self.writeInteger(UInt8.whitespace)
5861
self.writeInteger(UInt8.v)
@@ -101,6 +104,23 @@ extension ByteBuffer {
101104
self.writeInteger(UInt8.R)
102105
}
103106
}
107+
108+
if let arithmeticMode = flags.arithmeticMode {
109+
self.writeInteger(UInt8.whitespace)
110+
self.writeInteger(UInt8.M)
111+
switch arithmeticMode {
112+
case .decrement(let delta):
113+
self.writeInteger(UInt8.decrement)
114+
self.writeInteger(UInt8.whitespace)
115+
self.writeInteger(UInt8.D)
116+
self.writeIntegerAsASCII(delta)
117+
case .increment(let delta):
118+
self.writeInteger(UInt8.increment)
119+
self.writeInteger(UInt8.whitespace)
120+
self.writeInteger(UInt8.D)
121+
self.writeIntegerAsASCII(delta)
122+
}
123+
}
104124
}
105125
}
106126

Sources/SwiftMemcache/Extensions/UInt8+Characters.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ extension UInt8 {
2020
static var s: UInt8 = .init(ascii: "s")
2121
static var g: UInt8 = .init(ascii: "g")
2222
static var d: UInt8 = .init(ascii: "d")
23+
static var a: UInt8 = .init(ascii: "a")
2324
static var v: UInt8 = .init(ascii: "v")
2425
static var T: UInt8 = .init(ascii: "T")
2526
static var M: UInt8 = .init(ascii: "M")
2627
static var P: UInt8 = .init(ascii: "P")
2728
static var A: UInt8 = .init(ascii: "A")
2829
static var E: UInt8 = .init(ascii: "E")
2930
static var R: UInt8 = .init(ascii: "R")
31+
static var D: UInt8 = .init(ascii: "D")
3032
static var zero: UInt8 = .init(ascii: "0")
3133
static var nine: UInt8 = .init(ascii: "9")
34+
static var increment: UInt8 = .init(ascii: "+")
35+
static var decrement: UInt8 = .init(ascii: "-")
3236
}

Sources/SwiftMemcache/MemcachedConnection.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,4 +424,62 @@ public actor MemcachedConnection {
424424
throw MemcachedConnectionError.connectionShutdown
425425
}
426426
}
427+
428+
// MARK: - Increment a Value
429+
430+
/// Increment the value for an existing key in the Memcache server by a specified amount.
431+
///
432+
/// - Parameters:
433+
/// - key: The key for the value to increment.
434+
/// - amount: The `Int` amount to increment the value by. Must be larger than 0.
435+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
436+
public func increment(_ key: String, amount: Int) async throws {
437+
// Ensure the amount is greater than 0
438+
precondition(amount > 0, "Amount to increment should be larger than 0")
439+
440+
switch self.state {
441+
case .initial(_, _, _, _),
442+
.running:
443+
444+
var flags = MemcachedFlags()
445+
flags.arithmeticMode = .increment(amount)
446+
447+
let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags)
448+
let request = MemcachedRequest.arithmetic(command)
449+
450+
_ = try await self.sendRequest(request)
451+
452+
case .finished:
453+
throw MemcachedConnectionError.connectionShutdown
454+
}
455+
}
456+
457+
// MARK: - Decrement a Value
458+
459+
/// Decrement the value for an existing key in the Memcache server by a specified amount.
460+
///
461+
/// - Parameters:
462+
/// - key: The key for the value to decrement.
463+
/// - amount: The `Int` amount to decrement the value by. Must be larger than 0.
464+
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
465+
public func decrement(_ key: String, amount: Int) async throws {
466+
// Ensure the amount is greater than 0
467+
precondition(amount > 0, "Amount to decrement should be larger than 0")
468+
469+
switch self.state {
470+
case .initial(_, _, _, _),
471+
.running:
472+
473+
var flags = MemcachedFlags()
474+
flags.arithmeticMode = .decrement(amount)
475+
476+
let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags)
477+
let request = MemcachedRequest.arithmetic(command)
478+
479+
_ = try await self.sendRequest(request)
480+
481+
case .finished:
482+
throw MemcachedConnectionError.connectionShutdown
483+
}
484+
}
427485
}

Sources/SwiftMemcache/MemcachedFlags.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ struct MemcachedFlags {
3737
/// The default mode is 'set'.
3838
var storageMode: StorageMode?
3939

40+
/// Flag 'M' for the 'ma' (meta arithmetic) command.
41+
///
42+
/// Represents the mode of the 'ma' command, which determines the behavior of the arithmetic operation.
43+
var arithmeticMode: ArithmeticMode?
44+
4045
init() {}
4146
}
4247

@@ -60,4 +65,12 @@ enum StorageMode: Equatable, Hashable {
6065
case replace
6166
}
6267

68+
/// Enum representing the mode for the 'ma' (meta arithmetic) command in Memcached (corresponding to the 'M' flag).
69+
enum ArithmeticMode: Equatable, Hashable {
70+
/// 'increment' command. If applied, it increases the numerical value of the item.
71+
case increment(Int)
72+
/// 'decrement' command. If applied, it decreases the numerical value of the item.
73+
case decrement(Int)
74+
}
75+
6376
extension MemcachedFlags: Hashable {}

Sources/SwiftMemcache/MemcachedRequest.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ enum MemcachedRequest {
2929
let key: String
3030
}
3131

32+
struct ArithmeticCommand {
33+
let key: String
34+
var flags: MemcachedFlags
35+
}
36+
3237
case set(SetCommand)
3338
case get(GetCommand)
3439
case delete(DeleteCommand)
40+
case arithmetic(ArithmeticCommand)
3541
}

Sources/SwiftMemcache/MemcachedRequestEncoder.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ struct MemcachedRequestEncoder: MessageToByteEncoder {
7373
out.writeInteger(UInt8.whitespace)
7474
out.writeBytes(command.key.utf8)
7575

76+
// write separator
77+
out.writeInteger(UInt8.carriageReturn)
78+
out.writeInteger(UInt8.newline)
79+
80+
case .arithmetic(let command):
81+
precondition(!command.key.isEmpty, "Key must not be empty")
82+
83+
// write command and key
84+
out.writeInteger(UInt8.m)
85+
out.writeInteger(UInt8.a)
86+
out.writeInteger(UInt8.whitespace)
87+
out.writeBytes(command.key.utf8)
88+
89+
// write flags if there are any
90+
out.writeMemcachedFlags(flags: command.flags)
91+
7692
// write separator
7793
out.writeInteger(UInt8.carriageReturn)
7894
out.writeInteger(UInt8.newline)

Sources/SwiftMemcache/MemcachedValue.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ extension MemcachedValue where Self: FixedWidthInteger {
3333
///
3434
/// - Parameter buffer: The ByteBuffer to which the integer should be written.
3535
public func writeToBuffer(_ buffer: inout ByteBuffer) {
36-
buffer.writeInteger(self)
36+
buffer.writeIntegerAsASCII(self)
3737
}
3838

3939
/// Reads a FixedWidthInteger from a ByteBuffer.
4040
///
4141
/// - Parameter buffer: The ByteBuffer from which the value should be read.
4242
public static func readFromBuffer(_ buffer: inout ByteBuffer) -> Self? {
43-
return buffer.readInteger()
43+
return buffer.readIntegerFromASCII()
4444
}
4545
}
4646

Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,62 @@ final class MemcachedIntegrationTest: XCTestCase {
434434
}
435435
}
436436

437+
func testIncrementValue() async throws {
438+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
439+
defer {
440+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
441+
}
442+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
443+
444+
try await withThrowingTaskGroup(of: Void.self) { group in
445+
group.addTask { try await memcachedConnection.run() }
446+
447+
// Set key and initial value
448+
let initialValue = 1
449+
try await memcachedConnection.set("increment", value: initialValue)
450+
451+
// Increment value
452+
let incrementAmount = 100
453+
try await memcachedConnection.increment("increment", amount: incrementAmount)
454+
455+
// Get new value
456+
let newValue: Int? = try await memcachedConnection.get("increment")
457+
458+
// Check if new value is equal to initial value plus increment amount
459+
XCTAssertEqual(newValue, initialValue + incrementAmount, "Incremented value is incorrect")
460+
461+
group.cancelAll()
462+
}
463+
}
464+
465+
func testDecrementValue() async throws {
466+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
467+
defer {
468+
XCTAssertNoThrow(try! group.syncShutdownGracefully())
469+
}
470+
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)
471+
472+
try await withThrowingTaskGroup(of: Void.self) { group in
473+
group.addTask { try await memcachedConnection.run() }
474+
475+
// Set key and initial value
476+
let initialValue = 100
477+
try await memcachedConnection.set("decrement", value: initialValue)
478+
479+
// Increment value
480+
let decrementAmount = 10
481+
try await memcachedConnection.decrement("decrement", amount: decrementAmount)
482+
483+
// Get new value
484+
let newValue: Int? = try await memcachedConnection.get("decrement")
485+
486+
// Check if new value is equal to initial value plus increment amount
487+
XCTAssertEqual(newValue, initialValue - decrementAmount, "Incremented value is incorrect")
488+
489+
group.cancelAll()
490+
}
491+
}
492+
437493
func testMemcachedConnectionWithUInt() async throws {
438494
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
439495
defer {

Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,32 @@ final class MemcachedRequestEncoderTests: XCTestCase {
169169
let expectedEncodedData = "md foo\r\n"
170170
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
171171
}
172+
173+
func testEncodeIncrementRequest() {
174+
// Prepare a MemcachedRequest
175+
var flags = MemcachedFlags()
176+
flags.arithmeticMode = .increment(100)
177+
let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags)
178+
let request = MemcachedRequest.arithmetic(command)
179+
180+
// pass our request through the encoder
181+
let outBuffer = self.encodeRequest(request)
182+
183+
let expectedEncodedData = "ma foo M+ D100\r\n"
184+
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
185+
}
186+
187+
func testEncodeDecrementRequest() {
188+
// Prepare a MemcachedRequest
189+
var flags = MemcachedFlags()
190+
flags.arithmeticMode = .decrement(100)
191+
let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags)
192+
let request = MemcachedRequest.arithmetic(command)
193+
194+
// pass our request through the encoder
195+
let outBuffer = self.encodeRequest(request)
196+
197+
let expectedEncodedData = "ma foo M- D100\r\n"
198+
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
199+
}
172200
}

0 commit comments

Comments
 (0)