Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,17 @@ extension ByteBuffer {
}

if let storageMode = flags.storageMode {
self.writeInteger(UInt8.whitespace)
self.writeInteger(UInt8.M)
switch storageMode {
case .add:
self.writeInteger(UInt8.E)
case .append:
self.writeInteger(UInt8.whitespace)
self.writeInteger(UInt8.M)
self.writeInteger(UInt8.A)
case .prepend:
self.writeInteger(UInt8.whitespace)
self.writeInteger(UInt8.M)
self.writeInteger(UInt8.P)
case .replace:
self.writeInteger(UInt8.R)
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftMemcache/Extensions/UInt8+Characters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension UInt8 {
static var M: UInt8 = .init(ascii: "M")
static var P: UInt8 = .init(ascii: "P")
static var A: UInt8 = .init(ascii: "A")
static var E: UInt8 = .init(ascii: "E")
static var R: UInt8 = .init(ascii: "R")
static var zero: UInt8 = .init(ascii: "0")
static var nine: UInt8 = .init(ascii: "9")
}
75 changes: 75 additions & 0 deletions Sources/SwiftMemcache/MemcachedConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public actor MemcachedConnection {
case unexpectedNilResponse
/// Indicates that the key was not found.
case keyNotFound
/// Indicates that the key already exist
case keyExist
}

private var state: State
Expand Down Expand Up @@ -340,4 +342,77 @@ public actor MemcachedConnection {
throw MemcachedConnectionError.connectionShutdown
}
}

// MARK: - Adding a Value

/// Adds a value to a new key in the Memcached server.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Adds a value to a new key in the Memcached server.
/// Adds a new key-value pair in the Memcached server.
/// The operation will fail if the key already exists.
///
/// - Parameters:
/// - key: The key to add the value to.
/// - value: The `MemcachedValue` to add.
/// - Throws: A `MemcachedConnectionError.connectionShutdown` if the connection to the Memcached server is shut down.
/// - Throws: A `MemcachedConnectionError.keyExist` if the key already exists in the Memcached server.
/// - Throws: A `MemcachedConnectionError.unexpectedNilResponse` if an unexpected response code is returned.
public func add(_ key: String, value: some MemcachedValue) async throws {
switch self.state {
case .initial(_, let bufferAllocator, _, _),
.running(let bufferAllocator, _, _, _):

var buffer = bufferAllocator.buffer(capacity: 0)
value.writeToBuffer(&buffer)
var flags: MemcachedFlags

flags = MemcachedFlags()
flags.storageMode = .add

let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
let request = MemcachedRequest.set(command)

let response = try await sendRequest(request)

switch response.returnCode {
case .HD:
return
case .NS:
throw MemcachedConnectionError.keyExist
default:
throw MemcachedConnectionError.unexpectedNilResponse
}

case .finished:
throw MemcachedConnectionError.connectionShutdown
}
}

// MARK: - Replacing a Value

/// Replace the value for an existing key in the Memcache server.
/// The operation will fail if the key does not exist.
///
/// - Parameters:
/// - key: The key to replace the value for.
/// - value: The `MemcachedValue` to replace.
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
public func replace(_ key: String, value: some MemcachedValue) async throws {
switch self.state {
case .initial(_, let bufferAllocator, _, _),
.running(let bufferAllocator, _, _, _):

var buffer = bufferAllocator.buffer(capacity: 0)
value.writeToBuffer(&buffer)
var flags: MemcachedFlags

flags = MemcachedFlags()
flags.storageMode = .replace

let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags)
let request = MemcachedRequest.set(command)

_ = try await self.sendRequest(request)

case .finished:
throw MemcachedConnectionError.connectionShutdown
}
}
}
4 changes: 4 additions & 0 deletions Sources/SwiftMemcache/MemcachedFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ public enum TimeToLive: Equatable, Hashable {

/// Enum representing the Memcached 'ms' (meta set) command modes (corresponding to the 'M' flag).
public enum StorageMode: Equatable, Hashable {
/// The "add" command. If the item exists, LRU is bumped and NS is returned.
case add
/// The 'append' command. If the item exists, append the new value to its data.
case append
/// The 'prepend' command. If the item exists, prepend the new value to its data.
case prepend
/// The "replace" command. The new value is set only if the item already exists.
case replace
}

extension MemcachedFlags: Hashable {}
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,56 @@ final class MemcachedIntegrationTest: XCTestCase {
}
}

func testAddValue() async throws {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try! group.syncShutdownGracefully())
}
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)

try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await memcachedConnection.run() }

// Add a value to a key
let addValue = "foo"

try await memcachedConnection.delete("adds")
try await memcachedConnection.add("adds", value: addValue)

// Get value for the key after add operation
let addedValue: String? = try await memcachedConnection.get("adds")
XCTAssertEqual(addedValue, addValue, "Received value should be the same as the added value")

group.cancelAll()
}
}

func testReplaceValue() async throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we have a test where the key/value pair doesn't exist yet

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try! group.syncShutdownGracefully())
}
let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group)

try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await memcachedConnection.run() }

// Set key and initial value
let initialValue = "foo"
try await memcachedConnection.set("greet", value: initialValue)

// Replace value for the key
let replaceValue = "hi"
try await memcachedConnection.replace("greet", value: replaceValue)

// Get value for the key after replace operation
let replacedValue: String? = try await memcachedConnection.get("greet")
XCTAssertEqual(replacedValue, replaceValue, "Received value should be the same as the replaceValue")

group.cancelAll()
}
}

func testMemcachedConnectionWithUInt() async throws {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
Expand Down
20 changes: 20 additions & 0 deletions Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ final class MemcachedFlagsTests: XCTestCase {
}
}

func testStorageModeAdd() {
var flags = MemcachedFlags()
flags.storageMode = .add
if case .add? = flags.storageMode {
XCTAssertTrue(true)
} else {
XCTFail("Flag storageMode is not .add")
}
}

func testStorageModeAppend() {
var flags = MemcachedFlags()
flags.storageMode = .append
Expand All @@ -57,4 +67,14 @@ final class MemcachedFlagsTests: XCTestCase {
XCTFail("Flag storageMode is not .prepend")
}
}

func testStorageModeReplace() {
var flags = MemcachedFlags()
flags.storageMode = .replace
if case .replace? = flags.storageMode {
XCTAssertTrue(true)
} else {
XCTFail("Flag storageMode is not .replace")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,38 +48,37 @@ final class MemcachedRequestEncoderTests: XCTestCase {
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}

func testEncodeAppendRequest() {
func testEncodeStorageRequest(withMode mode: StorageMode, expectedEncodedData: String) {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
buffer.writeString("hi")

var flags = MemcachedFlags()
flags.storageMode = .append
flags.storageMode = mode
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
let request = MemcachedRequest.set(command)

// pass our request through the encoder
let outBuffer = self.encodeRequest(request)

let expectedEncodedData = "ms foo 2 MA\r\nhi\r\n"
// assert the encoded request
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}

func testEncodePrependRequest() {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
buffer.writeString("hi")
func testEncodeAppendRequest() {
self.testEncodeStorageRequest(withMode: .append, expectedEncodedData: "ms foo 2 MA\r\nhi\r\n")
}

var flags = MemcachedFlags()
flags.storageMode = .prepend
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer, flags: flags)
let request = MemcachedRequest.set(command)
func testEncodePrependRequest() {
self.testEncodeStorageRequest(withMode: .prepend, expectedEncodedData: "ms foo 2 MP\r\nhi\r\n")
}

// pass our request through the encoder
let outBuffer = self.encodeRequest(request)
func testEncodeAddRequest() {
self.testEncodeStorageRequest(withMode: .add, expectedEncodedData: "ms foo 2 ME\r\nhi\r\n")
}

let expectedEncodedData = "ms foo 2 MP\r\nhi\r\n"
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
func testEncodeReplaceRequest() {
self.testEncodeStorageRequest(withMode: .replace, expectedEncodedData: "ms foo 2 MR\r\nhi\r\n")
}

func testEncodeTouchRequest() {
Expand Down