Skip to content

Implement Append and Prepend Support #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ extension ByteBuffer {
}
}
}

if let storageMode = flags.storageMode {
switch storageMode {
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)
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftMemcache/Extensions/UInt8+Characters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ extension UInt8 {
static var d: UInt8 = .init(ascii: "d")
static var v: UInt8 = .init(ascii: "v")
static var T: UInt8 = .init(ascii: "T")
static var M: UInt8 = .init(ascii: "M")
static var P: UInt8 = .init(ascii: "P")
static var A: UInt8 = .init(ascii: "A")
static var zero: UInt8 = .init(ascii: "0")
static var nine: UInt8 = .init(ascii: "9")
}
60 changes: 60 additions & 0 deletions Sources/SwiftMemcache/MemcachedConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,64 @@ public actor MemcachedConnection {
throw MemcachedConnectionError.connectionShutdown
}
}

// MARK: - Prepending a Value

/// Prepend a value to an existing key in the Memcache server.
///
/// - Parameters:
/// - key: The key to prepend the value to.
/// - value: The `MemcachedValue` to prepend.
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
public func prepend(_ 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 = .prepend

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
}
}

// MARK: - Appending a Value

/// Append a value to an existing key in the Memcache server.
///
/// - Parameters:
/// - key: The key to append the value to.
/// - value: The `MemcachedValue` to append.
/// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down.
public func append(_ 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 = .append

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
}
}
}
14 changes: 14 additions & 0 deletions Sources/SwiftMemcache/MemcachedFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ struct MemcachedFlags {
/// If set, the item is considered to be expired after this number of seconds.
var timeToLive: TimeToLive?

/// Mode for the 'ms' (meta set) command (corresponding to the 'M' flag).
///
/// Represents the mode of the 'ms' command, which determines the behavior of the data operation.
/// The default mode is 'set'.
var storageMode: StorageMode?

init() {}
}

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

/// Enum representing the Memcached 'ms' (meta set) command modes (corresponding to the 'M' flag).
public enum StorageMode: Equatable, Hashable {
/// 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
}

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

func testPrependValue() 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() }

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

// Prepend value to key
let prependValue = "Hi"
try await memcachedConnection.prepend("greet", value: prependValue)

// Get value for key after prepend operation
let updatedValue: String? = try await memcachedConnection.get("greet")
XCTAssertEqual(updatedValue, prependValue + initialValue, "Received value should be the same as the concatenation of prependValue and initialValue")

group.cancelAll()
}
}

func testAppendValue() 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() }

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

// Append value to key
let appendValue = "foo"
try await memcachedConnection.append("greet", value: appendValue)

// Get value for key after append operation
let updatedValue: String? = try await memcachedConnection.get("greet")
XCTAssertEqual(updatedValue, initialValue + appendValue, "Received value should be the same as the concatenation of initialValue and appendValue")

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 @@ -37,4 +37,24 @@ final class MemcachedFlagsTests: XCTestCase {
XCTFail("Flag timeToLive is nil")
}
}

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

func testStorageModePrepend() {
var flags = MemcachedFlags()
flags.storageMode = .prepend
if case .prepend? = flags.storageMode {
XCTAssertTrue(true)
} else {
XCTFail("Flag storageMode is not .prepend")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ final class MemcachedRequestEncoderTests: XCTestCase {
self.encoder = MemcachedRequestEncoder()
}

func encodeRequest(_ request: MemcachedRequest) -> ByteBuffer {
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
return outBuffer
}

func testEncodeSetRequest() {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
Expand All @@ -32,17 +42,46 @@ final class MemcachedRequestEncoderTests: XCTestCase {
let request = MemcachedRequest.set(command)

// pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
let outBuffer = self.encodeRequest(request)

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

func testEncodeAppendRequest() {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
buffer.writeString("hi")

var flags = MemcachedFlags()
flags.storageMode = .append
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"
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}

func testEncodePrependRequest() {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
buffer.writeString("hi")

var flags = MemcachedFlags()
flags.storageMode = .prepend
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 MP\r\nhi\r\n"
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}

func testEncodeTouchRequest() {
// Prepare a MemcachedRequest
var flags = MemcachedFlags()
Expand All @@ -53,12 +92,7 @@ final class MemcachedRequestEncoderTests: XCTestCase {
let request = MemcachedRequest.get(command)

// pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
let outBuffer = self.encodeRequest(request)

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

// pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
let outBuffer = self.encodeRequest(request)

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

// pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
let outBuffer = self.encodeRequest(request)

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

let request = MemcachedRequest.get(command)

// Pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
// pass our request through the encoder
let outBuffer = self.encodeRequest(request)

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

// Pass our request through the encoder
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
do {
try self.encoder.encode(data: request, out: &outBuffer)
} catch {
XCTFail("Encoding failed with error: \(error)")
}
// pass our request through the encoder
let outBuffer = self.encodeRequest(request)

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