Skip to content
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ let package = Package(
name: "SwiftMemcache",
dependencies: [
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOEmbedded", package: "swift-nio"),
.product(name: "Logging", package: "swift-log"),
]
),
Expand Down
27 changes: 27 additions & 0 deletions Sources/SwiftMemcache/Extensions/ByteBuffer+IntegerAsASCII.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import NIOCore

extension ByteBuffer {
/// Write `integer` into this `ByteBuffer` as ASCII digits, without leading zeros, moving the writer index forward appropriately.
///
/// - parameters:
/// - integer: The integer to serialize.
@inlinable
mutating func writeIntegerAsASCII(_ integer: some FixedWidthInteger) {
let string = String(integer)
self.writeString(string)
}
}
38 changes: 38 additions & 0 deletions Sources/SwiftMemcache/Extensions/UInt16+ResponseCodes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

typealias ResponseStatus = UInt16

extension ResponseStatus {
// generates a 16-bit code from two ASCII characters
static func generateCode(from characters: (UInt8, UInt8)) -> ResponseStatus {
return ResponseStatus(characters.0) << 8 | ResponseStatus(characters.1)
}

static let stored = generateCode(from: (.init(ascii: "H"), .init(ascii: "D")))
static let notStored = generateCode(from: (.init(ascii: "N"), .init(ascii: "S")))
static let exists = generateCode(from: (.init(ascii: "E"), .init(ascii: "X")))
static let notFound = generateCode(from: (.init(ascii: "N"), .init(ascii: "F")))

init?(asciiValues: (UInt8, UInt8)) {
let code = ResponseStatus(asciiValues.0) << 8 | ResponseStatus(asciiValues.1)

switch code {
case ResponseStatus.stored, ResponseStatus.notStored, ResponseStatus.exists, ResponseStatus.notFound:
self = code
default:
preconditionFailure("Unrecognized response code.")
}
}
}
21 changes: 21 additions & 0 deletions Sources/SwiftMemcache/Extensions/UInt8+Characters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

extension UInt8 {
static var whitespace: UInt8 = .init(ascii: " ")
static var newline: UInt8 = .init(ascii: "\n")
static var carriageReturn: UInt8 = .init(ascii: "\r")
static var m: UInt8 = .init(ascii: "m")
static var s: UInt8 = .init(ascii: "s")
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,14 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore

enum MemcachedRequest {
struct SetCommand {
let key: String
var value: ByteBuffer
}

case set(SetCommand)
}
47 changes: 47 additions & 0 deletions Sources/SwiftMemcache/MemcachedRequestEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOPosix

struct MemcachedRequestEncoder: MessageToByteEncoder {
typealias OutboundIn = MemcachedRequest

func encode(data: MemcachedRequest, out: inout ByteBuffer) throws {
switch data {
case .set(var command):
precondition(!command.key.isEmpty, "Key must not be empty")

// write command and key
out.writeInteger(UInt8.m)
out.writeInteger(UInt8.s)
out.writeInteger(UInt8.whitespace)
out.writeBytes(command.key.utf8)
out.writeInteger(UInt8.whitespace)

// write value length
let length = command.value.readableBytes
out.writeIntegerAsASCII(length)

// write separator
out.writeInteger(UInt8.carriageReturn)
out.writeInteger(UInt8.newline)

// write value and end line
out.writeBuffer(&command.value)
out.writeInteger(UInt8.carriageReturn)
out.writeInteger(UInt8.newline)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,14 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore

enum MemcachedResponse {
struct SetResponse {
let status: ResponseStatus
let flags: ByteBuffer?
}

case set(SetResponse)
}
58 changes: 58 additions & 0 deletions Sources/SwiftMemcache/MemcachedResponseDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOPosix

struct MemcachedResponseDecoder: ByteToMessageDecoder {
typealias InboundOut = MemcachedResponse

var cumulationBuffer: ByteBuffer?

func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
// Ensure the buffer has at least 3 bytes (minimum for a response code and newline)
guard buffer.readableBytes >= 3 else {
return .needMoreData
}

guard let asciiValue1 = buffer.readInteger(as: UInt8.self),
let asciiValue2 = buffer.readInteger(as: UInt8.self),
let responseCode = ResponseStatus(asciiValues: (asciiValue1, asciiValue2)) else {
preconditionFailure("Response code could not be read.")
}

var flags: ByteBuffer?

// Check if there's a whitespace character, this indicates flags are present
if buffer.readableBytes > 2, buffer.getInteger(at: buffer.readerIndex, as: UInt8.self) == UInt8.whitespace {
buffer.moveReaderIndex(forwardBy: 1)

// -2 for \r\n
flags = buffer.readSlice(length: buffer.readableBytes - 2)
}

guard buffer.readInteger(as: UInt8.self) == UInt8.carriageReturn,
buffer.readInteger(as: UInt8.self) == UInt8.newline else {
preconditionFailure("Line ending '\r\n' not found after the flags.")
}

let setResponse = MemcachedResponse.SetResponse(status: responseCode, flags: flags)
context.fireChannelRead(self.wrapInboundOut(.set(setResponse)))
return .continue
}

func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState {
return try self.decode(context: context, buffer: &buffer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOPosix
@testable import SwiftMemcache
import XCTest

final class MemcachedIntegrationTest: XCTestCase {
var channel: ClientBootstrap!
var group: EventLoopGroup!

override func setUp() {
super.setUp()
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.channel = ClientBootstrap(group: self.group)
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.channelInitializer { channel in
return channel.pipeline.addHandlers([MessageToByteHandler(MemcachedRequestEncoder()), ByteToMessageHandler(MemcachedResponseDecoder())])
}
}

override func tearDown() {
XCTAssertNoThrow(try self.group.syncShutdownGracefully())
super.tearDown()
}

class ResponseHandler: ChannelInboundHandler {
typealias InboundIn = MemcachedResponse

let p: EventLoopPromise<MemcachedResponse>

init(p: EventLoopPromise<MemcachedResponse>) {
self.p = p
}

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let response = self.unwrapInboundIn(data)
self.p.succeed(response)
}
}

func testConnectionToMemcachedServer() throws {
do {
let connection = try channel.connect(host: "memcached", port: 11211).wait()
XCTAssertNotNil(connection)

// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 3)
buffer.writeString("hi")
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer)
let request = MemcachedRequest.set(command)

// Write the request to the connection
_ = connection.write(request)

// Prepare the promise for the response
let promise = connection.eventLoop.makePromise(of: MemcachedResponse.self)
let responseHandler = ResponseHandler(p: promise)
_ = connection.pipeline.addHandler(responseHandler)

// Flush and then read the response from the server
connection.flush()
connection.read()

// Wait for the promise to be fulfilled
let response = try promise.futureResult.wait()

// Check the response from the server.
switch response {
case .set(let setResponse):
print("Response status: \(setResponse.status)")
}

} catch {
XCTFail("Failed to connect to Memcached server: \(error)")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import NIOCore
@testable import SwiftMemcache
import XCTest

final class MemcachedRequestEncoderTests: XCTestCase {
var encoder: MemcachedRequestEncoder!

override func setUp() {
super.setUp()
self.encoder = MemcachedRequestEncoder()
}

func testEncodeSetRequest() {
// Prepare a MemcachedRequest
var buffer = ByteBufferAllocator().buffer(capacity: 2)
buffer.writeString("hi")
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer)
let request = MemcachedRequest.set(command)

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

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