From 764e3f7cecf1187d322e2642bf522e15ff09271e Mon Sep 17 00:00:00 2001 From: dkz2 Date: Sat, 5 Aug 2023 21:32:17 -0500 Subject: [PATCH 01/15] add + replace #25 --- .../Extensions/ByteBuffer+SwiftMemcache.swift | 10 ++- .../Extensions/UInt8+Characters.swift | 2 + .../SwiftMemcache/MemcachedConnection.swift | 75 +++++++++++++++++++ Sources/SwiftMemcache/MemcachedFlags.swift | 4 + .../MemcachedIntegrationTests.swift | 49 ++++++++++++ .../UnitTest/MemcachedFlagsTests.swift | 20 +++++ .../MemcachedRequestEncoderTests.swift | 29 ++++--- 7 files changed, 170 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift index 5df7c6d..aed4895 100644 --- a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift +++ b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift @@ -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) } } } diff --git a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift index e77f5bd..2fb0c2e 100644 --- a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift +++ b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift @@ -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") } diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 0dd79c4..b7bfd6c 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -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 @@ -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. + /// 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 + } + } } diff --git a/Sources/SwiftMemcache/MemcachedFlags.swift b/Sources/SwiftMemcache/MemcachedFlags.swift index b112bbd..53ec0ec 100644 --- a/Sources/SwiftMemcache/MemcachedFlags.swift +++ b/Sources/SwiftMemcache/MemcachedFlags.swift @@ -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 {} diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 9fd3320..e738393 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -306,6 +306,55 @@ 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 { + 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 { diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift index 5d51533..0d68881 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift @@ -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 @@ -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") + } + } } diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift index 2734d76..cdcf069 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift @@ -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() { From 882b5a0773d7915c31e096f7ec94e02405555dd5 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Sat, 5 Aug 2023 22:22:06 -0500 Subject: [PATCH 02/15] soundness --- .../IntegrationTest/MemcachedIntegrationTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index e738393..166e51b 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -318,6 +318,7 @@ final class MemcachedIntegrationTest: XCTestCase { // Add a value to a key let addValue = "foo" + try await memcachedConnection.delete("adds") try await memcachedConnection.add("adds", value: addValue) From f504be3a90736249c8cde0763f0a52d910165073 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Sun, 6 Aug 2023 13:32:13 -0500 Subject: [PATCH 03/15] more test for add + replace --- .../SwiftMemcache/MemcachedConnection.swift | 13 +++- Sources/SwiftMemcache/MemcachedFlags.swift | 2 +- .../MemcachedIntegrationTests.swift | 61 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index b7bfd6c..4a380e6 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -345,7 +345,7 @@ public actor MemcachedConnection { // MARK: - Adding a Value - /// 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: @@ -409,7 +409,16 @@ public actor MemcachedConnection { let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) let request = MemcachedRequest.set(command) - _ = try await self.sendRequest(request) + let response = try await sendRequest(request) + + switch response.returnCode { + case .HD: + return + case .NS: + throw MemcachedConnectionError.keyNotFound + default: + throw MemcachedConnectionError.unexpectedNilResponse + } case .finished: throw MemcachedConnectionError.connectionShutdown diff --git a/Sources/SwiftMemcache/MemcachedFlags.swift b/Sources/SwiftMemcache/MemcachedFlags.swift index 53ec0ec..69c464d 100644 --- a/Sources/SwiftMemcache/MemcachedFlags.swift +++ b/Sources/SwiftMemcache/MemcachedFlags.swift @@ -49,7 +49,7 @@ public enum TimeToLive: Equatable, Hashable { } /// Enum representing the Memcached 'ms' (meta set) command modes (corresponding to the 'M' flag). -public enum StorageMode: Equatable, Hashable { +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. diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 166e51b..49945d6 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -330,6 +330,39 @@ final class MemcachedIntegrationTest: XCTestCase { } } + func testAddValueKeyExists() 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 initialValue = "foo" + let newValue = "bar" + + // Ensure the key is clean and add an initial value + try await memcachedConnection.delete("adds") + try await memcachedConnection.add("adds", value: initialValue) + + do { + // Attempt to add a new value to the existing key + try await memcachedConnection.add("adds", value: newValue) + XCTFail("Expected an error indicating the key exists, but no error was thrown.") + } catch { + // Check if the error description or localized description matches the expected error + if "\(error)" != "keyExist" { + XCTFail("Unexpected error: \(error)") + } + } + + group.cancelAll() + } + } + func testReplaceValue() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { @@ -356,6 +389,34 @@ final class MemcachedIntegrationTest: XCTestCase { } } + func testReplaceNonExistentKey() async throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try! group.syncShutdownGracefully()) + } + let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { try await memcachedConnection.run() } + + do { + // Ensure the key is clean + try await memcachedConnection.delete("nonExistentKey") + // Attempt to replace value for a non-existent key + let replaceValue = "testValue" + try await memcachedConnection.replace("nonExistentKey", value: replaceValue) + XCTFail("Expected an error indicating the key was not found, but no error was thrown.") + } catch { + // Check if the error description or localized description matches the expected error + if "\(error)" != "keyNotFound" { + XCTFail("Unexpected error: \(error)") + } + } + + group.cancelAll() + } + } + func testMemcachedConnectionWithUInt() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { From b12415300b9087484017204cefe1ad5be3d7002c Mon Sep 17 00:00:00 2001 From: dkz2 Date: Sun, 6 Aug 2023 13:41:19 -0500 Subject: [PATCH 04/15] build faiure fix? --- .../MemcachedIntegrationTests.swift | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 49945d6..628e28b 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -319,7 +319,16 @@ final class MemcachedIntegrationTest: XCTestCase { // Add a value to a key let addValue = "foo" - try await memcachedConnection.delete("adds") + // Attempt to delete the key, but ignore the error if it doesn't exist + do { + try await memcachedConnection.delete("adds") + } catch { + if "\(error)" != "keyNotFound" { + throw error + } + } + + // Proceed with adding the key-value pair try await memcachedConnection.add("adds", value: addValue) // Get value for the key after add operation @@ -344,8 +353,16 @@ final class MemcachedIntegrationTest: XCTestCase { let initialValue = "foo" let newValue = "bar" - // Ensure the key is clean and add an initial value - try await memcachedConnection.delete("adds") + // Attempt to delete the key, but ignore the error if it doesn't exist + do { + try await memcachedConnection.delete("adds") + } catch { + if "\(error)" != "keyNotFound" { + throw error + } + } + + // Set an initial value for the key try await memcachedConnection.add("adds", value: initialValue) do { From 0bb4f8b10a1cb21e7737f4c9013ff4558190f717 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Tue, 8 Aug 2023 12:02:21 -0500 Subject: [PATCH 05/15] closes #26 --- .../Extensions/ByteBuffer+SwiftMemcache.swift | 16 ++++++ .../Extensions/UInt8+Characters.swift | 4 ++ .../SwiftMemcache/MemcachedConnection.swift | 54 ++++++++++++++++++ Sources/SwiftMemcache/MemcachedFlags.swift | 18 ++++++ Sources/SwiftMemcache/MemcachedRequest.swift | 6 ++ .../MemcachedRequestEncoder.swift | 16 ++++++ Sources/SwiftMemcache/MemcachedValue.swift | 5 +- .../MemcachedIntegrationTests.swift | 56 +++++++++++++++++++ .../MemcachedRequestEncoderTests.swift | 30 ++++++++++ 9 files changed, 203 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift index aed4895..48cbbf3 100644 --- a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift +++ b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift @@ -101,6 +101,22 @@ extension ByteBuffer { self.writeInteger(UInt8.R) } } + + if let arithmeticMode = flags.arithmeticMode { + self.writeInteger(UInt8.whitespace) + self.writeInteger(UInt8.M) + switch arithmeticMode { + case .decrement: + self.writeInteger(UInt8.decrement) + case .increment: + self.writeInteger(UInt8.increment) + } + if let arithmeticDelta = flags.arithmeticDelta { + self.writeInteger(UInt8.whitespace) + self.writeInteger(UInt8.D) + self.writeIntegerAsASCII(arithmeticDelta) + } + } } } diff --git a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift index 2fb0c2e..06c671d 100644 --- a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift +++ b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift @@ -20,6 +20,7 @@ extension UInt8 { static var s: UInt8 = .init(ascii: "s") static var g: UInt8 = .init(ascii: "g") static var d: UInt8 = .init(ascii: "d") + static var a: UInt8 = .init(ascii: "a") static var v: UInt8 = .init(ascii: "v") static var T: UInt8 = .init(ascii: "T") static var M: UInt8 = .init(ascii: "M") @@ -27,6 +28,9 @@ extension UInt8 { static var A: UInt8 = .init(ascii: "A") static var E: UInt8 = .init(ascii: "E") static var R: UInt8 = .init(ascii: "R") + static var D: UInt8 = .init(ascii: "D") static var zero: UInt8 = .init(ascii: "0") static var nine: UInt8 = .init(ascii: "9") + static var increment: UInt8 = .init(ascii: "+") + static var decrement: UInt8 = .init(ascii: "-") } diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 4a380e6..1af984b 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -424,4 +424,58 @@ public actor MemcachedConnection { throw MemcachedConnectionError.connectionShutdown } } + + // MARK: - Increment a Value + + /// Increment the value for an existing key in the Memcache server by a specified amount. + /// + /// - Parameters: + /// - key: The key for the value to increment. + /// - amount: The `UInt64` amount to increment the value by. + /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. + public func increment(_ key: String, amount: UInt64) async throws { + switch self.state { + case .initial(_, _, _, _), + .running: + + var flags = MemcachedFlags() + flags.arithmeticMode = .increment + flags.arithmeticDelta = amount + + let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) + let request = MemcachedRequest.arithmetic(command) + + _ = try await self.sendRequest(request) + + case .finished: + throw MemcachedConnectionError.connectionShutdown + } + } + + // MARK: - Decrement a Value + + /// Decrement the value for an existing key in the Memcache server by a specified amount. + /// + /// - Parameters: + /// - key: The key for the value to decrement. + /// - amount: The `UInt64` amount to decrement the value by. + /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. + public func decrement(_ key: String, amount: UInt64) async throws { + switch self.state { + case .initial(_, _, _, _), + .running: + + var flags = MemcachedFlags() + flags.arithmeticMode = .decrement + flags.arithmeticDelta = amount + + let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) + let request = MemcachedRequest.arithmetic(command) + + _ = try await self.sendRequest(request) + + case .finished: + throw MemcachedConnectionError.connectionShutdown + } + } } diff --git a/Sources/SwiftMemcache/MemcachedFlags.swift b/Sources/SwiftMemcache/MemcachedFlags.swift index 69c464d..192238c 100644 --- a/Sources/SwiftMemcache/MemcachedFlags.swift +++ b/Sources/SwiftMemcache/MemcachedFlags.swift @@ -37,6 +37,16 @@ struct MemcachedFlags { /// The default mode is 'set'. var storageMode: StorageMode? + /// Flag 'M' for the 'ma' (meta arithmetic) command. + /// + /// Represents the mode of the 'ma' command, which determines the behavior of the arithmetic operation. + var arithmeticMode: ArithmeticMode? + + /// Flag 'D' for the 'ma' (meta arithmetic) command. + /// + /// Represents the delta to apply to the 'ma' command. The default value is 1. + var arithmeticDelta: UInt64? + init() {} } @@ -60,4 +70,12 @@ enum StorageMode: Equatable, Hashable { case replace } +/// Enum representing the mode for the 'ma' (meta arithmetic) command in Memcached (corresponding to the 'M' flag). +enum ArithmeticMode: Equatable, Hashable { + /// 'increment' command. If applied, it increases the numerical value of the item. + case increment + /// 'decrement' command. If applied, it decreases the numerical value of the item. + case decrement +} + extension MemcachedFlags: Hashable {} diff --git a/Sources/SwiftMemcache/MemcachedRequest.swift b/Sources/SwiftMemcache/MemcachedRequest.swift index 232d0b7..5b9b03e 100644 --- a/Sources/SwiftMemcache/MemcachedRequest.swift +++ b/Sources/SwiftMemcache/MemcachedRequest.swift @@ -29,7 +29,13 @@ enum MemcachedRequest { let key: String } + struct ArithmeticCommand { + let key: String + var flags: MemcachedFlags + } + case set(SetCommand) case get(GetCommand) case delete(DeleteCommand) + case arithmetic(ArithmeticCommand) } diff --git a/Sources/SwiftMemcache/MemcachedRequestEncoder.swift b/Sources/SwiftMemcache/MemcachedRequestEncoder.swift index 1389bf0..274cdb3 100644 --- a/Sources/SwiftMemcache/MemcachedRequestEncoder.swift +++ b/Sources/SwiftMemcache/MemcachedRequestEncoder.swift @@ -73,6 +73,22 @@ struct MemcachedRequestEncoder: MessageToByteEncoder { out.writeInteger(UInt8.whitespace) out.writeBytes(command.key.utf8) + // write separator + out.writeInteger(UInt8.carriageReturn) + out.writeInteger(UInt8.newline) + + case .arithmetic(let command): + precondition(!command.key.isEmpty, "Key must not be empty") + + // write command and key + out.writeInteger(UInt8.m) + out.writeInteger(UInt8.a) + out.writeInteger(UInt8.whitespace) + out.writeBytes(command.key.utf8) + + // write flags if there are any + out.writeMemcachedFlags(flags: command.flags) + // write separator out.writeInteger(UInt8.carriageReturn) out.writeInteger(UInt8.newline) diff --git a/Sources/SwiftMemcache/MemcachedValue.swift b/Sources/SwiftMemcache/MemcachedValue.swift index f68b365..465cea7 100644 --- a/Sources/SwiftMemcache/MemcachedValue.swift +++ b/Sources/SwiftMemcache/MemcachedValue.swift @@ -33,14 +33,15 @@ extension MemcachedValue where Self: FixedWidthInteger { /// /// - Parameter buffer: The ByteBuffer to which the integer should be written. public func writeToBuffer(_ buffer: inout ByteBuffer) { - buffer.writeInteger(self) + buffer.writeString(String(self)) } /// Reads a FixedWidthInteger from a ByteBuffer. /// /// - Parameter buffer: The ByteBuffer from which the value should be read. public static func readFromBuffer(_ buffer: inout ByteBuffer) -> Self? { - return buffer.readInteger() + guard let string = buffer.readString(length: buffer.readableBytes)?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil } + return Self(string) } } diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 628e28b..1fc037e 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -434,6 +434,62 @@ final class MemcachedIntegrationTest: XCTestCase { } } + func testIncrementValue() 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: UInt64 = 1 + try await memcachedConnection.set("increment", value: initialValue) + + // Increment value + let incrementAmount: UInt64 = 100 + try await memcachedConnection.increment("increment", amount: incrementAmount) + + // Get new value + let newValue: UInt64? = try await memcachedConnection.get("increment") + + // Check if new value is equal to initial value plus increment amount + XCTAssertEqual(newValue, initialValue + incrementAmount, "Incremented value is incorrect") + + group.cancelAll() + } + } + + func testDecrementValue() 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: UInt64 = 100 + try await memcachedConnection.set("decrement", value: initialValue) + + // Increment value + let decrementAmount: UInt64 = 10 + try await memcachedConnection.decrement("decrement", amount: decrementAmount) + + // Get new value + let newValue: UInt64? = try await memcachedConnection.get("decrement") + + // Check if new value is equal to initial value plus increment amount + XCTAssertEqual(newValue, initialValue - decrementAmount, "Incremented value is incorrect") + + group.cancelAll() + } + } + func testMemcachedConnectionWithUInt() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift index cdcf069..1f1328d 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift @@ -169,4 +169,34 @@ final class MemcachedRequestEncoderTests: XCTestCase { let expectedEncodedData = "md foo\r\n" XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData) } + + func testEncodeIncrementRequest() { + // Prepare a MemcachedRequest + var flags = MemcachedFlags() + flags.arithmeticMode = .increment + flags.arithmeticDelta = 100 + let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags) + let request = MemcachedRequest.arithmetic(command) + + // pass our request through the encoder + let outBuffer = self.encodeRequest(request) + + let expectedEncodedData = "ma foo M+ D100\r\n" + XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData) + } + + func testEncodeDecrementRequest() { + // Prepare a MemcachedRequest + var flags = MemcachedFlags() + flags.arithmeticMode = .decrement + flags.arithmeticDelta = 100 + let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags) + let request = MemcachedRequest.arithmetic(command) + + // pass our request through the encoder + let outBuffer = self.encodeRequest(request) + + let expectedEncodedData = "ma foo M- D100\r\n" + XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData) + } } From 1e79a89d92ae377edf37dde4e743581b1f816624 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Tue, 8 Aug 2023 12:08:41 -0500 Subject: [PATCH 06/15] mr conflict clean up --- .../SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift | 3 --- Sources/SwiftMemcache/Extensions/UInt8+Characters.swift | 3 --- Sources/SwiftMemcache/MemcachedConnection.swift | 3 --- Sources/SwiftMemcache/MemcachedFlags.swift | 3 --- .../IntegrationTest/MemcachedIntegrationTests.swift | 3 --- 5 files changed, 15 deletions(-) diff --git a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift index 00983c5..48cbbf3 100644 --- a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift +++ b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift @@ -99,7 +99,6 @@ extension ByteBuffer { self.writeInteger(UInt8.P) case .replace: self.writeInteger(UInt8.R) -<<<<<<< HEAD } } @@ -116,8 +115,6 @@ extension ByteBuffer { self.writeInteger(UInt8.whitespace) self.writeInteger(UInt8.D) self.writeIntegerAsASCII(arithmeticDelta) -======= ->>>>>>> 9ae24ae6c441eaa6db85fab9671433c65de999b4 } } } diff --git a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift index 24280cd..06c671d 100644 --- a/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift +++ b/Sources/SwiftMemcache/Extensions/UInt8+Characters.swift @@ -28,10 +28,7 @@ extension UInt8 { static var A: UInt8 = .init(ascii: "A") static var E: UInt8 = .init(ascii: "E") static var R: UInt8 = .init(ascii: "R") -<<<<<<< HEAD static var D: UInt8 = .init(ascii: "D") -======= ->>>>>>> 9ae24ae6c441eaa6db85fab9671433c65de999b4 static var zero: UInt8 = .init(ascii: "0") static var nine: UInt8 = .init(ascii: "9") static var increment: UInt8 = .init(ascii: "+") diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 54989e1..1af984b 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -424,7 +424,6 @@ public actor MemcachedConnection { throw MemcachedConnectionError.connectionShutdown } } -<<<<<<< HEAD // MARK: - Increment a Value @@ -479,6 +478,4 @@ public actor MemcachedConnection { throw MemcachedConnectionError.connectionShutdown } } -======= ->>>>>>> 9ae24ae6c441eaa6db85fab9671433c65de999b4 } diff --git a/Sources/SwiftMemcache/MemcachedFlags.swift b/Sources/SwiftMemcache/MemcachedFlags.swift index a8af6e6..192238c 100644 --- a/Sources/SwiftMemcache/MemcachedFlags.swift +++ b/Sources/SwiftMemcache/MemcachedFlags.swift @@ -68,7 +68,6 @@ enum StorageMode: Equatable, Hashable { case prepend /// The "replace" command. The new value is set only if the item already exists. case replace -<<<<<<< HEAD } /// Enum representing the mode for the 'ma' (meta arithmetic) command in Memcached (corresponding to the 'M' flag). @@ -77,8 +76,6 @@ enum ArithmeticMode: Equatable, Hashable { case increment /// 'decrement' command. If applied, it decreases the numerical value of the item. case decrement -======= ->>>>>>> 9ae24ae6c441eaa6db85fab9671433c65de999b4 } extension MemcachedFlags: Hashable {} diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 516b543..1fc037e 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -434,7 +434,6 @@ final class MemcachedIntegrationTest: XCTestCase { } } -<<<<<<< HEAD func testIncrementValue() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { @@ -491,8 +490,6 @@ final class MemcachedIntegrationTest: XCTestCase { } } -======= ->>>>>>> 9ae24ae6c441eaa6db85fab9671433c65de999b4 func testMemcachedConnectionWithUInt() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { From c486acc9f1b654e44ec21c461121904ae28961c5 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Tue, 8 Aug 2023 23:14:54 -0500 Subject: [PATCH 07/15] detla is now associated value --- .../Extensions/ByteBuffer+SwiftMemcache.swift | 11 +++++----- .../SwiftMemcache/MemcachedConnection.swift | 20 +++++++++++-------- Sources/SwiftMemcache/MemcachedFlags.swift | 9 ++------- .../MemcachedIntegrationTests.swift | 12 +++++------ .../MemcachedRequestEncoderTests.swift | 6 ++---- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift index 48cbbf3..ceedb40 100644 --- a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift +++ b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift @@ -106,15 +106,16 @@ extension ByteBuffer { self.writeInteger(UInt8.whitespace) self.writeInteger(UInt8.M) switch arithmeticMode { - case .decrement: + case .decrement(let delta): self.writeInteger(UInt8.decrement) - case .increment: + self.writeInteger(UInt8.whitespace) + self.writeInteger(UInt8.D) + self.writeIntegerAsASCII(delta) + case .increment(let delta): self.writeInteger(UInt8.increment) - } - if let arithmeticDelta = flags.arithmeticDelta { self.writeInteger(UInt8.whitespace) self.writeInteger(UInt8.D) - self.writeIntegerAsASCII(arithmeticDelta) + self.writeIntegerAsASCII(delta) } } } diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 1af984b..3af1e4d 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -431,16 +431,18 @@ public actor MemcachedConnection { /// /// - Parameters: /// - key: The key for the value to increment. - /// - amount: The `UInt64` amount to increment the value by. + /// - amount: The `Int` amount to increment the value by. Must be larger than 0. /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. - public func increment(_ key: String, amount: UInt64) async throws { + public func increment(_ key: String, amount: Int) async throws { + // Ensure the amount is greater than 0 + precondition(amount > 0, "Amount to increment should be larger than 0") + switch self.state { case .initial(_, _, _, _), .running: var flags = MemcachedFlags() - flags.arithmeticMode = .increment - flags.arithmeticDelta = amount + flags.arithmeticMode = .increment(amount) let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) let request = MemcachedRequest.arithmetic(command) @@ -458,16 +460,18 @@ public actor MemcachedConnection { /// /// - Parameters: /// - key: The key for the value to decrement. - /// - amount: The `UInt64` amount to decrement the value by. + /// - amount: The `Int` amount to decrement the value by. Must be larger than 0. /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. - public func decrement(_ key: String, amount: UInt64) async throws { + public func decrement(_ key: String, amount: Int) async throws { + // Ensure the amount is greater than 0 + precondition(amount > 0, "Amount to decrement should be larger than 0") + switch self.state { case .initial(_, _, _, _), .running: var flags = MemcachedFlags() - flags.arithmeticMode = .decrement - flags.arithmeticDelta = amount + flags.arithmeticMode = .decrement(amount) let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) let request = MemcachedRequest.arithmetic(command) diff --git a/Sources/SwiftMemcache/MemcachedFlags.swift b/Sources/SwiftMemcache/MemcachedFlags.swift index 192238c..e098b6f 100644 --- a/Sources/SwiftMemcache/MemcachedFlags.swift +++ b/Sources/SwiftMemcache/MemcachedFlags.swift @@ -42,11 +42,6 @@ struct MemcachedFlags { /// Represents the mode of the 'ma' command, which determines the behavior of the arithmetic operation. var arithmeticMode: ArithmeticMode? - /// Flag 'D' for the 'ma' (meta arithmetic) command. - /// - /// Represents the delta to apply to the 'ma' command. The default value is 1. - var arithmeticDelta: UInt64? - init() {} } @@ -73,9 +68,9 @@ enum StorageMode: Equatable, Hashable { /// Enum representing the mode for the 'ma' (meta arithmetic) command in Memcached (corresponding to the 'M' flag). enum ArithmeticMode: Equatable, Hashable { /// 'increment' command. If applied, it increases the numerical value of the item. - case increment + case increment(Int) /// 'decrement' command. If applied, it decreases the numerical value of the item. - case decrement + case decrement(Int) } extension MemcachedFlags: Hashable {} diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 1fc037e..685aca6 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -445,15 +445,15 @@ final class MemcachedIntegrationTest: XCTestCase { group.addTask { try await memcachedConnection.run() } // Set key and initial value - let initialValue: UInt64 = 1 + let initialValue = 1 try await memcachedConnection.set("increment", value: initialValue) // Increment value - let incrementAmount: UInt64 = 100 + let incrementAmount = 100 try await memcachedConnection.increment("increment", amount: incrementAmount) // Get new value - let newValue: UInt64? = try await memcachedConnection.get("increment") + let newValue: Int? = try await memcachedConnection.get("increment") // Check if new value is equal to initial value plus increment amount XCTAssertEqual(newValue, initialValue + incrementAmount, "Incremented value is incorrect") @@ -473,15 +473,15 @@ final class MemcachedIntegrationTest: XCTestCase { group.addTask { try await memcachedConnection.run() } // Set key and initial value - let initialValue: UInt64 = 100 + let initialValue = 100 try await memcachedConnection.set("decrement", value: initialValue) // Increment value - let decrementAmount: UInt64 = 10 + let decrementAmount = 10 try await memcachedConnection.decrement("decrement", amount: decrementAmount) // Get new value - let newValue: UInt64? = try await memcachedConnection.get("decrement") + let newValue: Int? = try await memcachedConnection.get("decrement") // Check if new value is equal to initial value plus increment amount XCTAssertEqual(newValue, initialValue - decrementAmount, "Incremented value is incorrect") diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift index 1f1328d..92de139 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift @@ -173,8 +173,7 @@ final class MemcachedRequestEncoderTests: XCTestCase { func testEncodeIncrementRequest() { // Prepare a MemcachedRequest var flags = MemcachedFlags() - flags.arithmeticMode = .increment - flags.arithmeticDelta = 100 + flags.arithmeticMode = .increment(100) let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags) let request = MemcachedRequest.arithmetic(command) @@ -188,8 +187,7 @@ final class MemcachedRequestEncoderTests: XCTestCase { func testEncodeDecrementRequest() { // Prepare a MemcachedRequest var flags = MemcachedFlags() - flags.arithmeticMode = .decrement - flags.arithmeticDelta = 100 + flags.arithmeticMode = .decrement(100) let command = MemcachedRequest.ArithmeticCommand(key: "foo", flags: flags) let request = MemcachedRequest.arithmetic(command) From f863db27458c66638772ddf4e26ee2445becc118 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Tue, 8 Aug 2023 23:17:27 -0500 Subject: [PATCH 08/15] precondition that both storage and arth mode are not set --- .../SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift index ceedb40..dc4044f 100644 --- a/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift +++ b/Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift @@ -53,6 +53,9 @@ extension ByteBuffer { /// - parameters: /// - flags: An instance of MemcachedFlags that holds the flags intended to be serialized and written to the ByteBuffer. mutating func writeMemcachedFlags(flags: MemcachedFlags) { + // Ensure that both storageMode and arithmeticMode aren't set at the same time. + precondition(!(flags.storageMode != nil && flags.arithmeticMode != nil), "Cannot specify both a storage and arithmetic mode.") + if let shouldReturnValue = flags.shouldReturnValue, shouldReturnValue { self.writeInteger(UInt8.whitespace) self.writeInteger(UInt8.v) From 153b018bc88e30c2e0fbf209771ee44f53759528 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Thu, 10 Aug 2023 12:56:43 -0500 Subject: [PATCH 09/15] make sure of writeIntAsASCII --- Sources/SwiftMemcache/MemcachedValue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftMemcache/MemcachedValue.swift b/Sources/SwiftMemcache/MemcachedValue.swift index 465cea7..3ab27e9 100644 --- a/Sources/SwiftMemcache/MemcachedValue.swift +++ b/Sources/SwiftMemcache/MemcachedValue.swift @@ -33,7 +33,7 @@ extension MemcachedValue where Self: FixedWidthInteger { /// /// - Parameter buffer: The ByteBuffer to which the integer should be written. public func writeToBuffer(_ buffer: inout ByteBuffer) { - buffer.writeString(String(self)) + buffer.writeIntegerAsASCII(self) } /// Reads a FixedWidthInteger from a ByteBuffer. From ea8a16ae938341705184850bc67bc343ae24ed91 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 11 Aug 2023 12:30:03 -0500 Subject: [PATCH 10/15] closes #30 --- .../SwiftMemcache/MemcachedConnection.swift | 184 ++++++++++---- Sources/SwiftMemcache/MemcachedError.swift | 231 ++++++++++++++++++ .../MemcachedIntegrationTests.swift | 43 +++- 3 files changed, 400 insertions(+), 58 deletions(-) create mode 100644 Sources/SwiftMemcache/MemcachedError.swift diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 3af1e4d..5f57149 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -53,18 +53,6 @@ public actor MemcachedConnection { case finished } - /// Enum representing the possible errors that can be encountered in `MemcachedConnection`. - enum MemcachedConnectionError: Error { - /// Indicates that the connection has shut down. - case connectionShutdown - /// Indicates that a nil response was received from the server. - case unexpectedNilResponse - /// Indicates that the key was not found. - case keyNotFound - /// Indicates that the key already exist - case keyExist - } - private var state: State /// Initialize a new MemcachedConnection. @@ -92,7 +80,12 @@ public actor MemcachedConnection { /// to the server is finished or the task that called this method is cancelled. public func run() async throws { guard case .initial(let eventLoopGroup, let bufferAllocator, let stream, let continuation) = state else { - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } let channel = try await ClientBootstrap(group: eventLoopGroup) @@ -128,7 +121,12 @@ public actor MemcachedConnection { case .running: self.state = .finished requestContinuation.finish() - continuation.resume(throwing: error) + continuation.resume(throwing: MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down while processing a request.", + cause: error, + location: MemcachedError.SourceLocation.here() + )) case .initial, .finished: break } @@ -151,14 +149,24 @@ public actor MemcachedConnection { case .enqueued: break case .dropped, .terminated: - continuation.resume(throwing: MemcachedConnectionError.connectionShutdown) + continuation.resume(throwing: MemcachedError( + code: .connectionShutdown, + message: "Unable to enqueue request due to the connection being dropped or terminated.", + cause: nil, + location: MemcachedError.SourceLocation.here() + )) default: break } } case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -184,10 +192,20 @@ public actor MemcachedConnection { if var unwrappedResponse = response { return Value.readFromBuffer(&unwrappedResponse) } else { - throw MemcachedConnectionError.unexpectedNilResponse + throw MemcachedError( + code: .unexpectedNilResponse, + message: "Received an unexpected nil response from the Memcached server.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -200,7 +218,7 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to update the time-to-live for. /// - newTimeToLive: The new time-to-live. - /// - Throws: A `MemcachedConnectionError` if the connection is shutdown or if there's an unexpected nil response. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func touch(_ key: String, newTimeToLive: TimeToLive) async throws { switch self.state { case .initial(_, _, _, _), @@ -215,7 +233,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -229,7 +252,7 @@ public actor MemcachedConnection { /// - expiration: An optional `TimeToLive` value specifying the TTL (Time-To-Live) for the key-value pair. /// If provided, the key-value pair will be removed from the cache after the specified TTL duration has passed. /// If not provided, the key-value pair will persist indefinitely in the cache. - /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func set(_ key: String, value: some MemcachedValue, timeToLive: TimeToLive = .indefinitely) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -248,7 +271,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -257,8 +285,9 @@ public actor MemcachedConnection { /// Delete the value for a key from the Memcache server. /// /// - Parameter key: The key of the item to be deleted. - /// - Throws: A `MemcachedConnectionError.connectionShutdown` error if the connection to the Memcache server is shut down. - /// - Throws: A `MemcachedConnectionError.unexpectedNilResponse` error if the key was not found or if an unexpected response code was returned. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` with the code `.keyNotFound` if the key was not found. + /// - Throws: A `MemcachedError` with the code `.unexpectedNilResponse` if an unexpected response code was returned. public func delete(_ key: String) async throws { switch self.state { case .initial(_, _, _, _), @@ -273,13 +302,28 @@ public actor MemcachedConnection { case .HD: return case .NF: - throw MemcachedConnectionError.keyNotFound + throw MemcachedError( + code: .keyNotFound, + message: "The specified key was not found.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) default: - throw MemcachedConnectionError.unexpectedNilResponse + throw MemcachedError( + code: .unexpectedNilResponse, + message: "Received an unexpected nil response from the Memcached server.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -290,7 +334,7 @@ public actor MemcachedConnection { /// - 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. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func prepend(_ key: String, value: some MemcachedValue) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -309,7 +353,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -320,7 +369,7 @@ public actor MemcachedConnection { /// - 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. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func append(_ key: String, value: some MemcachedValue) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -339,7 +388,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -351,9 +405,9 @@ public actor MemcachedConnection { /// - 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. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` with the code `.keyExist` if the key already exist. + /// - Throws: A `MemcachedError` with the code `.unexpectedNilResponse` if an unexpected response code was returned. public func add(_ key: String, value: some MemcachedValue) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -375,13 +429,28 @@ public actor MemcachedConnection { case .HD: return case .NS: - throw MemcachedConnectionError.keyExist + throw MemcachedError( + code: .keyExist, + message: "The specified key already exist.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) default: - throw MemcachedConnectionError.unexpectedNilResponse + throw MemcachedError( + code: .unexpectedNilResponse, + message: "Received an unexpected nil response from the Memcached server.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -393,7 +462,7 @@ public actor MemcachedConnection { /// - 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. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func replace(_ key: String, value: some MemcachedValue) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -415,13 +484,28 @@ public actor MemcachedConnection { case .HD: return case .NS: - throw MemcachedConnectionError.keyNotFound + throw MemcachedError( + code: .keyNotFound, + message: "The specified key was not found.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) default: - throw MemcachedConnectionError.unexpectedNilResponse + throw MemcachedError( + code: .unexpectedNilResponse, + message: "Received an unexpected nil response from the Memcached server.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -432,7 +516,7 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key for the value to increment. /// - amount: The `Int` amount to increment the value by. Must be larger than 0. - /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func increment(_ key: String, amount: Int) async throws { // Ensure the amount is greater than 0 precondition(amount > 0, "Amount to increment should be larger than 0") @@ -450,7 +534,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } @@ -461,7 +550,7 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key for the value to decrement. /// - amount: The `Int` amount to decrement the value by. Must be larger than 0. - /// - Throws: A `MemcachedConnectionError` if the connection to the Memcached server is shut down. + /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. public func decrement(_ key: String, amount: Int) async throws { // Ensure the amount is greater than 0 precondition(amount > 0, "Amount to decrement should be larger than 0") @@ -479,7 +568,12 @@ public actor MemcachedConnection { _ = try await self.sendRequest(request) case .finished: - throw MemcachedConnectionError.connectionShutdown + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has shut down.", + cause: nil, + location: MemcachedError.SourceLocation.here() + ) } } } diff --git a/Sources/SwiftMemcache/MemcachedError.swift b/Sources/SwiftMemcache/MemcachedError.swift new file mode 100644 index 0000000..479c97a --- /dev/null +++ b/Sources/SwiftMemcache/MemcachedError.swift @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the swift-memcache-gsoc open source project +// +// Copyright (c) 2023 Apple Inc. and the swift-memcache-gsoc project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of swift-memcache-gsoc project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// An error thrown as a result of interaction with memcache +public struct MemcachedError: Error, @unchecked Sendable { + // Note: @unchecked because we use a backing class for storage. + + private var storage: Storage + private mutating func ensureStorageIsUnique() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + private final class Storage { + var code: Code + var message: String + var cause: Error? + var location: SourceLocation + + init(code: Code, message: String, cause: Error?, location: SourceLocation) { + self.code = code + self.message = message + self.cause = cause + self.location = location + } + + func copy() -> Self { + return Self( + code: self.code, + message: self.message, + cause: self.cause, + location: self.location + ) + } + } + + /// A high-level error code to provide broad a classification. + public var code: Code { + get { self.storage.code } + set { + self.ensureStorageIsUnique() + self.storage.code = newValue + } + } + + /// A message describing what went wrong and how it may be remedied. + public var message: String { + get { self.storage.message } + set { + self.ensureStorageIsUnique() + self.storage.message = newValue + } + } + + /// An underlying error which caused the operation to fail. This may include additional details + /// about the root cause of the failure. + public var cause: Error? { + get { self.storage.cause } + set { + self.ensureStorageIsUnique() + self.storage.cause = newValue + } + } + + /// The location from which this error was thrown. + public var location: SourceLocation { + get { self.storage.location } + set { + self.ensureStorageIsUnique() + self.storage.location = newValue + } + } + + public init( + code: Code, + message: String, + cause: Error?, + location: SourceLocation + ) { + self.storage = Storage(code: code, message: message, cause: cause, location: location) + } + + /// Creates a ``MemcachedError`` by wrapping the given `cause` and its location and code. + internal init(message: String, wrapping cause: MemcachedError) { + self.init(code: cause.code, message: message, cause: cause, location: cause.location) + } +} + +extension MemcachedError: CustomStringConvertible { + public var description: String { + if let cause = self.cause { + return "\(self.code): \(self.message) (\(cause))" + } else { + return "\(self.code): \(self.message)" + } + } +} + +extension MemcachedError: CustomDebugStringConvertible { + public var debugDescription: String { + if let cause = self.cause { + return "\(String(reflecting: self.code)): \(String(reflecting: self.message)) (\(String(reflecting: cause)))" + } else { + return "\(String(reflecting: self.code)): \(String(reflecting: self.message))" + } + } +} + +extension MemcachedError { + private func detailedDescriptionLines() -> [String] { + // Build up a tree-like description of the error. This allows nested causes to be formatted + // correctly, especially when they are also MemcachedError. + var lines = [ + "MemcachedError: \(self.code)", + "├─ Reason: \(self.message)", + ] + + if let error = self.cause as? MemcachedError { + lines.append("├─ Cause:") + let causeLines = error.detailedDescriptionLines() + // We know this will never be empty. + lines.append("│ └─ \(causeLines.first!)") + lines.append(contentsOf: causeLines.dropFirst().map { "│ \($0)" }) + } else if let error = self.cause { + lines.append("├─ Cause: \(String(reflecting: error))") + } + + lines.append("└─ Source location: \(self.location.function) (\(self.location.file):\(self.location.line))") + + return lines + } + + /// A detailed multi-line description of the error. + /// + /// - Returns: A multi-line description of the error. + public func detailedDescription() -> String { + return self.detailedDescriptionLines().joined(separator: "\n") + } +} + +extension MemcachedError { + /// A high level indication of the kind of error being thrown. + public struct Code: Hashable, Sendable, CustomStringConvertible { + private enum Wrapped: Hashable, Sendable, CustomStringConvertible { + case connectionShutdown + case unexpectedNilResponse + case keyNotFound + case keyExist + + var description: String { + switch self { + case .connectionShutdown: + return "Connection shutdown" + case .unexpectedNilResponse: + return "Unexpected nil response" + case .keyNotFound: + return "Key not Found" + case .keyExist: + return "Key already Exist" + } + } + } + + public var description: String { + String(describing: self.code) + } + + private var code: Wrapped + private init(_ code: Wrapped) { + self.code = code + } + + /// The ``MemcacheConection`` is already shutdown. + public static var connectionShutdown: Self { + Self(.connectionShutdown) + } + + /// Indicates that a nil response was received from the server. + public static var unexpectedNilResponse: Self { + Self(.unexpectedNilResponse) + } + + /// Indicates that the key was not found. + public static var keyNotFound: Self { + Self(.keyNotFound) + } + + /// Indicates that the key already exists. + public static var keyExist: Self { + Self(.keyExist) + } + } + + /// A location within source code. + public struct SourceLocation: Sendable, Hashable { + /// The function in which the error was thrown. + public var function: String + + /// The file in which the error was thrown. + public var file: String + + /// The line on which the error was thrown. + public var line: Int + + public init(function: String, file: String, line: Int) { + self.function = function + self.file = file + self.line = line + } + + internal static func here( + function: String = #function, + file: String = #fileID, + line: Int = #line + ) -> Self { + return SourceLocation(function: function, file: file, line: line) + } + } +} diff --git a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift index 685aca6..d5f473b 100644 --- a/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift +++ b/Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift @@ -322,10 +322,10 @@ final class MemcachedIntegrationTest: XCTestCase { // Attempt to delete the key, but ignore the error if it doesn't exist do { try await memcachedConnection.delete("adds") + } catch let error as MemcachedError where error.code == .keyNotFound { + // Expected that the key might not exist, so just continue } catch { - if "\(error)" != "keyNotFound" { - throw error - } + XCTFail("Unexpected error during deletion: \(error)") } // Proceed with adding the key-value pair @@ -356,8 +356,11 @@ final class MemcachedIntegrationTest: XCTestCase { // Attempt to delete the key, but ignore the error if it doesn't exist do { try await memcachedConnection.delete("adds") - } catch { - if "\(error)" != "keyNotFound" { + } catch let error as MemcachedError { + switch error.code { + case .keyNotFound: + break + default: throw error } } @@ -369,9 +372,12 @@ final class MemcachedIntegrationTest: XCTestCase { // Attempt to add a new value to the existing key try await memcachedConnection.add("adds", value: newValue) XCTFail("Expected an error indicating the key exists, but no error was thrown.") - } catch { - // Check if the error description or localized description matches the expected error - if "\(error)" != "keyExist" { + } catch let error as MemcachedError { + switch error.code { + case .keyExist: + // Expected error + break + default: XCTFail("Unexpected error: \(error)") } } @@ -413,19 +419,30 @@ final class MemcachedIntegrationTest: XCTestCase { } let memcachedConnection = MemcachedConnection(host: "memcached", port: 11211, eventLoopGroup: group) - await withThrowingTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await memcachedConnection.run() } + // Ensure the key is clean do { - // Ensure the key is clean try await memcachedConnection.delete("nonExistentKey") + } catch let error as MemcachedError where error.code == .keyNotFound { + // Expected that the key might not exist, so just continue + } catch { + XCTFail("Unexpected error during deletion: \(error)") + return + } + + do { // Attempt to replace value for a non-existent key let replaceValue = "testValue" try await memcachedConnection.replace("nonExistentKey", value: replaceValue) XCTFail("Expected an error indicating the key was not found, but no error was thrown.") - } catch { - // Check if the error description or localized description matches the expected error - if "\(error)" != "keyNotFound" { + } catch let error as MemcachedError { + switch error.code { + case .keyNotFound: + // Expected error + break + default: XCTFail("Unexpected error: \(error)") } } From bcc69b7483843ad556634c7d5d29dbefb42ea8ee Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 11 Aug 2023 14:23:32 -0500 Subject: [PATCH 11/15] soundness --- Sources/SwiftMemcache/MemcachedError.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SwiftMemcache/MemcachedError.swift b/Sources/SwiftMemcache/MemcachedError.swift index 479c97a..8f30226 100644 --- a/Sources/SwiftMemcache/MemcachedError.swift +++ b/Sources/SwiftMemcache/MemcachedError.swift @@ -154,9 +154,13 @@ extension MemcachedError { /// A high level indication of the kind of error being thrown. public struct Code: Hashable, Sendable, CustomStringConvertible { private enum Wrapped: Hashable, Sendable, CustomStringConvertible { + /// Indicates that the connection has shut down. case connectionShutdown + /// Indicates that a nil response was received from the server. case unexpectedNilResponse + /// Indicates that the key was not found. case keyNotFound + /// Indicates that the key already exist case keyExist var description: String { From bf23544831a71eaa87a90d3b7df7018b6cc088ea Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 11 Aug 2023 15:53:26 -0500 Subject: [PATCH 12/15] added unit test --- .../UnitTest/MemcachedErrorTest.swift | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift new file mode 100644 index 0000000..c944b5b --- /dev/null +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the swift-memcache-gsoc open source project +// +// Copyright (c) 2023 Apple Inc. and the swift-memcache-gsoc project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of swift-memcache-gsoc project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import SwiftMemcache +import XCTest + +final class MemcachedErrorTests: XCTestCase { + func testInitialization() { + let location = MemcachedError.SourceLocation(function: "testFunction", file: "testFile.swift", line: 8) + + let error = MemcachedError(code: .keyNotFound, message: "Key not available", cause: nil, location: location) + + XCTAssertEqual(error.code.description, "Key not Found") + XCTAssertEqual(error.message, "Key not available") + XCTAssertEqual(error.location.function, "testFunction") + XCTAssertEqual(error.location.file, "testFile.swift") + XCTAssertEqual(error.location.line, 8) + } + + func testCustomStringConvertible() { + let location = MemcachedError.SourceLocation.here() + let causeError = MemcachedError(code: .unexpectedNilResponse, message: "No response", cause: nil, location: location) + let mainError = MemcachedError(code: .connectionShutdown, message: "Connection lost", cause: causeError, location: location) + + let description = mainError.description + XCTAssertTrue(description.contains("Connection shutdown")) + XCTAssertTrue(description.contains("Connection lost")) + XCTAssertTrue(description.contains("Unexpected nil response")) + } + + func testCustomDebugStringConvertible() { + let location = MemcachedError.SourceLocation.here() + let error = MemcachedError(code: .keyExist, message: "Key already present", cause: nil, location: location) + + let debugDescription = error.debugDescription + XCTAssertTrue(debugDescription.contains("Key already Exist")) + XCTAssertTrue(debugDescription.contains("Key already present")) + } + + func testDetailedDescription() { + let location = MemcachedError.SourceLocation.here() + let causeError = MemcachedError(code: .unexpectedNilResponse, message: "No response", cause: nil, location: location) + let mainError = MemcachedError(code: .connectionShutdown, message: "Connection lost", cause: causeError, location: location) + + let detailedDesc = mainError.detailedDescription() + + XCTAssertTrue(detailedDesc.contains(mainError.code.description)) + XCTAssertTrue(detailedDesc.contains(mainError.message)) + XCTAssertTrue(detailedDesc.contains(causeError.code.description)) + XCTAssertTrue(detailedDesc.contains(causeError.message)) + XCTAssertTrue(detailedDesc.contains(String(describing: location.line))) + XCTAssertTrue(detailedDesc.contains(location.file)) + XCTAssertTrue(detailedDesc.contains(location.function)) + } + + func testNestedErrorInitialization() { + let location = MemcachedError.SourceLocation.here() + let causeError = MemcachedError(code: .keyExist, message: "Key already present", cause: nil, location: location) + let mainError = MemcachedError(message: "A nested error", wrapping: causeError) + + XCTAssertEqual(mainError.message, "A nested error") + + if let unwrappedCause = mainError.cause as? MemcachedError { + XCTAssertEqual(unwrappedCause.code, causeError.code) + XCTAssertEqual(unwrappedCause.message, causeError.message) + XCTAssertEqual(unwrappedCause.location, causeError.location) + } else { + XCTFail("Expected mainError.cause to be of type MemcachedError") + } + } +} From e6bd8dc11a024004fef8a35da3bbe84b25cd245a Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 11 Aug 2023 18:51:55 -0500 Subject: [PATCH 13/15] unit test refactor no string formatting --- .../UnitTest/MemcachedErrorTest.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift index c944b5b..febcffe 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift @@ -34,9 +34,10 @@ final class MemcachedErrorTests: XCTestCase { let mainError = MemcachedError(code: .connectionShutdown, message: "Connection lost", cause: causeError, location: location) let description = mainError.description - XCTAssertTrue(description.contains("Connection shutdown")) - XCTAssertTrue(description.contains("Connection lost")) - XCTAssertTrue(description.contains("Unexpected nil response")) + + XCTAssertTrue(description.contains(mainError.code.description)) + XCTAssertTrue(description.contains(mainError.message)) + XCTAssertTrue(description.contains(causeError.code.description)) } func testCustomDebugStringConvertible() { @@ -44,8 +45,9 @@ final class MemcachedErrorTests: XCTestCase { let error = MemcachedError(code: .keyExist, message: "Key already present", cause: nil, location: location) let debugDescription = error.debugDescription - XCTAssertTrue(debugDescription.contains("Key already Exist")) - XCTAssertTrue(debugDescription.contains("Key already present")) + + XCTAssertTrue(debugDescription.contains(error.code.description)) + XCTAssertTrue(debugDescription.contains(error.message)) } func testDetailedDescription() { From fe93dfa5695386f43583ce17fcb1c574863cbbbd Mon Sep 17 00:00:00 2001 From: dkz2 Date: Mon, 14 Aug 2023 22:26:59 -0500 Subject: [PATCH 14/15] MemcachedConnection clean up --- .../SwiftMemcache/MemcachedConnection.swift | 402 +++++++----------- Sources/SwiftMemcache/MemcachedError.swift | 14 +- .../UnitTest/MemcachedErrorTest.swift | 4 +- 3 files changed, 170 insertions(+), 250 deletions(-) diff --git a/Sources/SwiftMemcache/MemcachedConnection.swift b/Sources/SwiftMemcache/MemcachedConnection.swift index 5f57149..e4c5374 100644 --- a/Sources/SwiftMemcache/MemcachedConnection.swift +++ b/Sources/SwiftMemcache/MemcachedConnection.swift @@ -82,9 +82,9 @@ public actor MemcachedConnection { guard case .initial(let eventLoopGroup, let bufferAllocator, let stream, let continuation) = state else { throw MemcachedError( code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + message: "The connection to the Memcached server has been shut down.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() ) } @@ -115,6 +115,15 @@ public actor MemcachedConnection { if let response = responseBuffer { continuation.resume(returning: response) + } else { + self.state = .finished + requestContinuation.finish() + continuation.resume(throwing: MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server was unexpectedly closed.", + cause: nil, + location: .here() + )) } } catch { switch self.state { @@ -125,7 +134,7 @@ public actor MemcachedConnection { code: .connectionShutdown, message: "The connection to the Memcached server has shut down while processing a request.", cause: error, - location: MemcachedError.SourceLocation.here() + location: .here() )) case .initial, .finished: break @@ -151,9 +160,9 @@ public actor MemcachedConnection { case .dropped, .terminated: continuation.resume(throwing: MemcachedError( code: .connectionShutdown, - message: "Unable to enqueue request due to the connection being dropped or terminated.", + message: "Unable to enqueue request due to the connection being shutdown.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() )) default: break @@ -163,9 +172,31 @@ public actor MemcachedConnection { case .finished: throw MemcachedError( code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + message: "The connection to the Memcached server has been shut down.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() + ) + } + } + + /// Retrieves the current `ByteBufferAllocator` based on the actor's state. + /// + /// - Returns: The current `ByteBufferAllocator` if the state is either `initial` or `running`. + /// - Throws: A `MemcachedError` if the connection state is `finished`, indicating the connection to the Memcached server has been shut down. + /// + /// The method abstracts the state management aspect, providing a convenient way to access the `ByteBufferAllocator` while + /// ensuring that the actor's state is appropriately checked. + private func getBufferAllocator() throws -> ByteBufferAllocator { + switch self.state { + case .initial(_, let bufferAllocator, _, _), + .running(let bufferAllocator, _, _, _): + return bufferAllocator + case .finished: + throw MemcachedError( + code: .connectionShutdown, + message: "The connection to the Memcached server has been shut down.", + cause: nil, + location: .here() ) } } @@ -176,35 +207,24 @@ public actor MemcachedConnection { /// /// - Parameter key: The key to fetch the value for. /// - Returns: A `Value` containing the fetched value, or `nil` if no value was found. + /// - Throws: A `MemcachedError` that indicates the failure. public func get(_ key: String, as valueType: Value.Type = Value.self) async throws -> Value? { - switch self.state { - case .initial(_, _, _, _), - .running: - - var flags = MemcachedFlags() - flags.shouldReturnValue = true - - let command = MemcachedRequest.GetCommand(key: key, flags: flags) - let request = MemcachedRequest.get(command) - - let response = try await sendRequest(request).value - - if var unwrappedResponse = response { - return Value.readFromBuffer(&unwrappedResponse) - } else { - throw MemcachedError( - code: .unexpectedNilResponse, - message: "Received an unexpected nil response from the Memcached server.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } - case .finished: + var flags = MemcachedFlags() + flags.shouldReturnValue = true + + let command = MemcachedRequest.GetCommand(key: key, flags: flags) + let request = MemcachedRequest.get(command) + + let response = try await sendRequest(request) + + if var unwrappedResponse = response.value { + return Value.readFromBuffer(&unwrappedResponse) + } else { throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + code: .protocolError, + message: "Received an unexpected return code \(response.returnCode) for a get request.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() ) } } @@ -218,28 +238,15 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to update the time-to-live for. /// - newTimeToLive: The new time-to-live. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func touch(_ key: String, newTimeToLive: TimeToLive) async throws { - switch self.state { - case .initial(_, _, _, _), - .running: + var flags = MemcachedFlags() + flags.timeToLive = newTimeToLive - var flags = MemcachedFlags() - flags.timeToLive = newTimeToLive + let command = MemcachedRequest.GetCommand(key: key, flags: flags) + let request = MemcachedRequest.get(command) - let command = MemcachedRequest.GetCommand(key: key, flags: flags) - let request = MemcachedRequest.get(command) - - _ = try await self.sendRequest(request) - - case .finished: - throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + _ = try await self.sendRequest(request) } // MARK: - Setting a Value @@ -252,7 +259,7 @@ public actor MemcachedConnection { /// - expiration: An optional `TimeToLive` value specifying the TTL (Time-To-Live) for the key-value pair. /// If provided, the key-value pair will be removed from the cache after the specified TTL duration has passed. /// If not provided, the key-value pair will persist indefinitely in the cache. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func set(_ key: String, value: some MemcachedValue, timeToLive: TimeToLive = .indefinitely) async throws { switch self.state { case .initial(_, let bufferAllocator, _, _), @@ -273,9 +280,9 @@ public actor MemcachedConnection { case .finished: throw MemcachedError( code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + message: "The connection to the Memcached server has been shut down.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() ) } } @@ -285,44 +292,29 @@ public actor MemcachedConnection { /// Delete the value for a key from the Memcache server. /// /// - Parameter key: The key of the item to be deleted. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. - /// - Throws: A `MemcachedError` with the code `.keyNotFound` if the key was not found. - /// - Throws: A `MemcachedError` with the code `.unexpectedNilResponse` if an unexpected response code was returned. + /// - Throws: A `MemcachedError` that indicates the failure. public func delete(_ key: String) async throws { - switch self.state { - case .initial(_, _, _, _), - .running: - - let command = MemcachedRequest.DeleteCommand(key: key) - let request = MemcachedRequest.delete(command) - - let response = try await sendRequest(request) - - switch response.returnCode { - case .HD: - return - case .NF: - throw MemcachedError( - code: .keyNotFound, - message: "The specified key was not found.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - default: - throw MemcachedError( - code: .unexpectedNilResponse, - message: "Received an unexpected nil response from the Memcached server.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + let command = MemcachedRequest.DeleteCommand(key: key) + let request = MemcachedRequest.delete(command) - case .finished: + let response = try await sendRequest(request) + + switch response.returnCode { + case .HD: + return + case .NF: throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + code: .keyNotFound, + message: "The specified key was not found.", + cause: nil, + location: .here() + ) + default: + throw MemcachedError( + code: .protocolError, + message: "Received an unexpected return code \(response.returnCode) for a delete request.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() ) } } @@ -334,32 +326,21 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to prepend the value to. /// - value: The `MemcachedValue` to prepend. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func prepend(_ key: String, value: some MemcachedValue) async throws { - switch self.state { - case .initial(_, let bufferAllocator, _, _), - .running(let bufferAllocator, _, _, _): + let bufferAllocator = try getBufferAllocator() - var buffer = bufferAllocator.buffer(capacity: 0) - value.writeToBuffer(&buffer) - var flags: MemcachedFlags - - flags = MemcachedFlags() - flags.storageMode = .prepend + var buffer = bufferAllocator.buffer(capacity: 0) + value.writeToBuffer(&buffer) + var flags: MemcachedFlags - let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) - let request = MemcachedRequest.set(command) + flags = MemcachedFlags() + flags.storageMode = .prepend - _ = try await self.sendRequest(request) + let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) + let request = MemcachedRequest.set(command) - case .finished: - throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + _ = try await self.sendRequest(request) } // MARK: - Appending a Value @@ -369,32 +350,21 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to append the value to. /// - value: The `MemcachedValue` to append. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func append(_ key: String, value: some MemcachedValue) async throws { - switch self.state { - case .initial(_, let bufferAllocator, _, _), - .running(let bufferAllocator, _, _, _): + let bufferAllocator = try getBufferAllocator() - var buffer = bufferAllocator.buffer(capacity: 0) - value.writeToBuffer(&buffer) - var flags: MemcachedFlags - - flags = MemcachedFlags() - flags.storageMode = .append + var buffer = bufferAllocator.buffer(capacity: 0) + value.writeToBuffer(&buffer) + var flags: MemcachedFlags - let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) - let request = MemcachedRequest.set(command) + flags = MemcachedFlags() + flags.storageMode = .append - _ = try await self.sendRequest(request) + let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) + let request = MemcachedRequest.set(command) - case .finished: - throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + _ = try await self.sendRequest(request) } // MARK: - Adding a Value @@ -405,51 +375,38 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to add the value to. /// - value: The `MemcachedValue` to add. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. - /// - Throws: A `MemcachedError` with the code `.keyExist` if the key already exist. - /// - Throws: A `MemcachedError` with the code `.unexpectedNilResponse` if an unexpected response code was returned. + /// - Throws: A `MemcachedError` that indicates the failure. public func add(_ key: String, value: some MemcachedValue) async throws { - switch self.state { - case .initial(_, let bufferAllocator, _, _), - .running(let bufferAllocator, _, _, _): + let bufferAllocator = try getBufferAllocator() - var buffer = bufferAllocator.buffer(capacity: 0) - value.writeToBuffer(&buffer) - var flags: MemcachedFlags + var buffer = bufferAllocator.buffer(capacity: 0) + value.writeToBuffer(&buffer) + var flags: MemcachedFlags - flags = MemcachedFlags() - flags.storageMode = .add + flags = MemcachedFlags() + flags.storageMode = .add - let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) - let request = MemcachedRequest.set(command) + 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 MemcachedError( - code: .keyExist, - message: "The specified key already exist.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - default: - throw MemcachedError( - code: .unexpectedNilResponse, - message: "Received an unexpected nil response from the Memcached server.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + let response = try await sendRequest(request) - case .finished: + switch response.returnCode { + case .HD: + return + case .NS: throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + code: .keyExist, + message: "The specified key already exist.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() + ) + default: + throw MemcachedError( + code: .protocolError, + message: "Received an unexpected return code \(response.returnCode) for a add request.", + cause: nil, + location: .here() ) } } @@ -462,49 +419,38 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key to replace the value for. /// - value: The `MemcachedValue` to replace. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func replace(_ key: String, value: some MemcachedValue) async throws { - switch self.state { - case .initial(_, let bufferAllocator, _, _), - .running(let bufferAllocator, _, _, _): + let bufferAllocator = try getBufferAllocator() - var buffer = bufferAllocator.buffer(capacity: 0) - value.writeToBuffer(&buffer) - var flags: MemcachedFlags + var buffer = bufferAllocator.buffer(capacity: 0) + value.writeToBuffer(&buffer) + var flags: MemcachedFlags - flags = MemcachedFlags() - flags.storageMode = .replace + flags = MemcachedFlags() + flags.storageMode = .replace - let command = MemcachedRequest.SetCommand(key: key, value: buffer, flags: flags) - let request = MemcachedRequest.set(command) + 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 MemcachedError( - code: .keyNotFound, - message: "The specified key was not found.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - default: - throw MemcachedError( - code: .unexpectedNilResponse, - message: "Received an unexpected nil response from the Memcached server.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + let response = try await sendRequest(request) - case .finished: + switch response.returnCode { + case .HD: + return + case .NS: throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", + code: .keyNotFound, + message: "The specified key was not found.", cause: nil, - location: MemcachedError.SourceLocation.here() + location: .here() + ) + default: + throw MemcachedError( + code: .protocolError, + message: "Received an unexpected return code \(response.returnCode) for a replace request.", + cause: nil, + location: .here() ) } } @@ -516,31 +462,18 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key for the value to increment. /// - amount: The `Int` amount to increment the value by. Must be larger than 0. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func increment(_ key: String, amount: Int) async throws { // Ensure the amount is greater than 0 precondition(amount > 0, "Amount to increment should be larger than 0") - switch self.state { - case .initial(_, _, _, _), - .running: - - var flags = MemcachedFlags() - flags.arithmeticMode = .increment(amount) + var flags = MemcachedFlags() + flags.arithmeticMode = .increment(amount) - let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) - let request = MemcachedRequest.arithmetic(command) + let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) + let request = MemcachedRequest.arithmetic(command) - _ = try await self.sendRequest(request) - - case .finished: - throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + _ = try await self.sendRequest(request) } // MARK: - Decrement a Value @@ -550,30 +483,17 @@ public actor MemcachedConnection { /// - Parameters: /// - key: The key for the value to decrement. /// - amount: The `Int` amount to decrement the value by. Must be larger than 0. - /// - Throws: A `MemcachedError` with the code `.connectionShutdown` if the connection to the Memcache server is shut down. + /// - Throws: A `MemcachedError` that indicates the failure. public func decrement(_ key: String, amount: Int) async throws { // Ensure the amount is greater than 0 precondition(amount > 0, "Amount to decrement should be larger than 0") - switch self.state { - case .initial(_, _, _, _), - .running: - - var flags = MemcachedFlags() - flags.arithmeticMode = .decrement(amount) + var flags = MemcachedFlags() + flags.arithmeticMode = .decrement(amount) - let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) - let request = MemcachedRequest.arithmetic(command) + let command = MemcachedRequest.ArithmeticCommand(key: key, flags: flags) + let request = MemcachedRequest.arithmetic(command) - _ = try await self.sendRequest(request) - - case .finished: - throw MemcachedError( - code: .connectionShutdown, - message: "The connection to the Memcached server has shut down.", - cause: nil, - location: MemcachedError.SourceLocation.here() - ) - } + _ = try await self.sendRequest(request) } } diff --git a/Sources/SwiftMemcache/MemcachedError.swift b/Sources/SwiftMemcache/MemcachedError.swift index 8f30226..bbdb012 100644 --- a/Sources/SwiftMemcache/MemcachedError.swift +++ b/Sources/SwiftMemcache/MemcachedError.swift @@ -46,7 +46,7 @@ public struct MemcachedError: Error, @unchecked Sendable { } } - /// A high-level error code to provide broad a classification. + /// A high-level error code to provide a broad classification. public var code: Code { get { self.storage.code } set { @@ -156,8 +156,8 @@ extension MemcachedError { private enum Wrapped: Hashable, Sendable, CustomStringConvertible { /// Indicates that the connection has shut down. case connectionShutdown - /// Indicates that a nil response was received from the server. - case unexpectedNilResponse + /// Indicates that there was a violation or inconsistency in the expected Memcached protocol behavior. + case protocolError /// Indicates that the key was not found. case keyNotFound /// Indicates that the key already exist @@ -167,8 +167,8 @@ extension MemcachedError { switch self { case .connectionShutdown: return "Connection shutdown" - case .unexpectedNilResponse: - return "Unexpected nil response" + case .protocolError: + return "Protocol Error" case .keyNotFound: return "Key not Found" case .keyExist: @@ -192,8 +192,8 @@ extension MemcachedError { } /// Indicates that a nil response was received from the server. - public static var unexpectedNilResponse: Self { - Self(.unexpectedNilResponse) + public static var protocolError: Self { + Self(.protocolError) } /// Indicates that the key was not found. diff --git a/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift index febcffe..8307fb9 100644 --- a/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift +++ b/Tests/SwiftMemcacheTests/UnitTest/MemcachedErrorTest.swift @@ -30,7 +30,7 @@ final class MemcachedErrorTests: XCTestCase { func testCustomStringConvertible() { let location = MemcachedError.SourceLocation.here() - let causeError = MemcachedError(code: .unexpectedNilResponse, message: "No response", cause: nil, location: location) + let causeError = MemcachedError(code: .protocolError, message: "No response", cause: nil, location: location) let mainError = MemcachedError(code: .connectionShutdown, message: "Connection lost", cause: causeError, location: location) let description = mainError.description @@ -52,7 +52,7 @@ final class MemcachedErrorTests: XCTestCase { func testDetailedDescription() { let location = MemcachedError.SourceLocation.here() - let causeError = MemcachedError(code: .unexpectedNilResponse, message: "No response", cause: nil, location: location) + let causeError = MemcachedError(code: .protocolError, message: "No response", cause: nil, location: location) let mainError = MemcachedError(code: .connectionShutdown, message: "Connection lost", cause: causeError, location: location) let detailedDesc = mainError.detailedDescription() From 545e6433e3f932b6706d92a97ed2d295083a23fb Mon Sep 17 00:00:00 2001 From: dkz2 Date: Mon, 14 Aug 2023 22:30:22 -0500 Subject: [PATCH 15/15] merge main update --- Sources/SwiftMemcache/MemcachedRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftMemcache/MemcachedRequest.swift b/Sources/SwiftMemcache/MemcachedRequest.swift index 2a18efa..508371a 100644 --- a/Sources/SwiftMemcache/MemcachedRequest.swift +++ b/Sources/SwiftMemcache/MemcachedRequest.swift @@ -29,7 +29,7 @@ enum MemcachedRequest: Sendable { let key: String } - struct ArithmeticCommand { + struct ArithmeticCommand: Sendable { let key: String var flags: MemcachedFlags }