Skip to content
Merged
1 change: 1 addition & 0 deletions Sources/SwiftMemcache/Extensions/UInt8+Characters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ extension UInt8 {
static var carriageReturn: UInt8 = .init(ascii: "\r")
static var m: UInt8 = .init(ascii: "m")
static var s: UInt8 = .init(ascii: "s")
static var g: UInt8 = .init(ascii: "g")
}
9 changes: 9 additions & 0 deletions Sources/SwiftMemcache/MemcachedFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ enum MemcachedFlag {
/// v: return item value in <data block>
case v

init?(bytes: UInt8) {
switch bytes {
case 0x76:
self = .v
default:
return nil
}
}

var bytes: UInt8 {
switch self {
case .v:
Expand Down
6 changes: 6 additions & 0 deletions Sources/SwiftMemcache/MemcachedRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ enum MemcachedRequest {
var value: ByteBuffer
}

struct GetCommand {
let key: String
var flags: [MemcachedFlag]
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we want to use something more specialised than an array here. We probably should create a MemcachedFlags struct instead that holds all of the flags inline. We know what flags exists so we don't need the dynamism that array offers us.

}

case set(SetCommand)
case get(GetCommand)
}
19 changes: 19 additions & 0 deletions Sources/SwiftMemcache/MemcachedRequestEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ struct MemcachedRequestEncoder: MessageToByteEncoder {
out.writeBuffer(&command.value)
out.writeInteger(UInt8.carriageReturn)
out.writeInteger(UInt8.newline)

case .get(let command):
precondition(!command.key.isEmpty, "Key must not be empty")

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

// write flags if there are any
for flag in command.flags {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we should create MemcachedFlags struct and create a separate extension method on ByteBuffer that offers a mutating func write(flags: MemcachedFlags)

out.writeInteger(UInt8.whitespace)
out.writeInteger(flag.bytes)
}

// write separator
out.writeInteger(UInt8.carriageReturn)
out.writeInteger(UInt8.newline)
}
}
}
4 changes: 4 additions & 0 deletions Sources/SwiftMemcache/MemcachedResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import NIOCore

struct MemcachedResponse {
enum ReturnCode {
case HD
Expand Down Expand Up @@ -40,4 +42,6 @@ struct MemcachedResponse {

var returnCode: ReturnCode
var dataLength: UInt64?
var flags: [MemcachedFlag]?
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here let's not use an array and lets add a mutating func readMemcachedFlags() -> MemcachedFlags to ByteBuffer

var value: ByteBuffer?
}
45 changes: 41 additions & 4 deletions Sources/SwiftMemcache/MemcachedResponseDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
/// This error is thrown when EOF is encountered but there are still
/// readable bytes in the buffer, which can indicate a bad message.
case unexpectedEOF

/// This error is thrown when EOF is encountered but there is still an expected next step
/// in the decoder's state machine. This error suggests that the message ended prematurely,
/// possibly indicating a bad message.
case unexpectedNextStep(NextStep)

/// This error is thrown when an unexpected character is encountered in the buffer
/// during the decoding process.
case unexpectedCharacter(UInt8)
Expand All @@ -77,6 +75,7 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
/// Decode the next flag
case decodeNextFlag(MemcachedResponse.ReturnCode, UInt64?)
// TODO: Add a next step for decoding the response data if the return code is VA
case decodeValue(MemcachedResponse.ReturnCode, UInt64, [MemcachedFlag])
}

/// The action that the decoder will take in response to the current state of the ByteBuffer and the `NextStep`.
Expand Down Expand Up @@ -120,8 +119,24 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {

case .dataLengthOrFlag(let returnCode):
Copy link
Contributor

Choose a reason for hiding this comment

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

I am wondering if we should separate this into two steps one dataLength and one flag and we can decide what the next step is in the case above when we decoded the return code.

if returnCode == .VA {
// TODO: Implement decoding of data length
fatalError("Decoding for VA return code is not yet implemented")
guard let dataLength = buffer.readInteger(as: UInt64.self) else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct? Is the data length encoded as a UInt64 or rather written out as text?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you are correct, it is written out as text, so I will make that change as well!

return .waitForMoreBytes
}

let flags: [MemcachedFlag] = []
while let nextByte = buffer.readInteger(as: UInt8.self), nextByte != UInt8.whitespace {
// TODO: Implement decoding of flags
}

guard let nextByte = buffer.readInteger(as: UInt8.self),
nextByte == UInt8.carriageReturn,
let nextNextByte = buffer.readInteger(as: UInt8.self),
nextNextByte == UInt8.newline else {
return .waitForMoreBytes
}

self.nextStep = .decodeValue(returnCode, dataLength, flags)
return .continueDecodeLoop
}

guard let nextByte = buffer.readInteger(as: UInt8.self) else {
Expand Down Expand Up @@ -151,6 +166,28 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength)
self.nextStep = .returnCode
return .returnDecodedResponse(response)

case .decodeValue(let returnCode, let dataLength, let flags):
guard buffer.readableBytes >= dataLength else {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably check here for dataLength + carriageReturn + newLine. To make sure we are not decoding the data until we also have the returns

return .waitForMoreBytes
}

var data: ByteBuffer = .init()
for _ in 0..<dataLength {
let byte = buffer.readInteger(as: UInt8.self)
data.writeInteger(byte!)
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a method to read a slice of a buffer which we should use here readSlice(length:)

}

guard let nextByte = buffer.readInteger(as: UInt8.self),
nextByte == UInt8.carriageReturn,
let nextNextByte = buffer.readInteger(as: UInt8.self),
nextNextByte == UInt8.newline else {
return .waitForMoreBytes
Copy link
Contributor

Choose a reason for hiding this comment

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

When we change the logic here to guard that we have these bytes readable as well we should probably fatalError here because it should never happen

}

let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength, flags: flags, value: data)
self.nextStep = .returnCode
return .returnDecodedResponse(response)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,21 @@ final class MemcachedRequestEncoderTests: XCTestCase {
let expectedEncodedData = "ms foo 2\r\nhi\r\n"
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}

func testEncodeGetRequest() {
// Prepare a MemcachedRequest
let command = MemcachedRequest.GetCommand(key: "foo", flags: [.v])
let request = MemcachedRequest.get(command)

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

let expectedEncodedData = "mg foo v\r\n"
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,26 @@ final class MemcachedResponseDecoderTests: XCTestCase {

buffer.writeInteger(returnCode)

// If there's a data length, write it to the buffer.
// Write the data length <size> to the buffer.
if let dataLength = response.dataLength, response.returnCode == .VA {
buffer.writeInteger(UInt8.whitespace, as: UInt8.self)
buffer.writeInteger(dataLength, as: UInt64.self)
buffer.writeInteger(dataLength)
buffer.writeInteger(UInt8.whitespace)

// Add flags after dataLength
for flag in response.flags ?? [] {
buffer.writeInteger(flag.bytes)
buffer.writeInteger(UInt8.whitespace)
}

buffer.writeBytes([UInt8.carriageReturn, UInt8.newline])

// Write the value <data block> to the buffer if it exists
if let value = response.value {
var mutableValue = value
buffer.writeBuffer(&mutableValue)
}

buffer.writeBytes([UInt8.carriageReturn, UInt8.newline])
}

buffer.writeBytes([UInt8.carriageReturn, UInt8.newline])
Expand Down Expand Up @@ -94,4 +110,34 @@ final class MemcachedResponseDecoderTests: XCTestCase {
var buffer = self.makeMemcachedResponseByteBuffer(from: notFoundResponse)
try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .NF)
}

func testDecodeValueResponse() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we need a couple of more tests here that are trying to pass partial responses and assert that we return the right things.

let allocator = ByteBufferAllocator()
var valueBuffer = allocator.buffer(capacity: 8)
valueBuffer.writeString("hi")

let valueResponse = MemcachedResponse(returnCode: .VA, dataLength: 2, flags: [], value: valueBuffer)
var buffer = self.makeMemcachedResponseByteBuffer(from: valueResponse)

// Pass our response through the decoder
var output: MemcachedResponse? = nil
do {
output = try self.decoder.decode(buffer: &buffer)
} catch {
XCTFail("Decoding failed with error: \(error)")
}
// Check the decoded response
if let decoded = output {
XCTAssertEqual(decoded.returnCode, .VA)
XCTAssertEqual(decoded.dataLength, 2)
if let value = decoded.value {
var copiedBuffer = value
XCTAssertEqual(copiedBuffer.readString(length: Int(decoded.dataLength!)), "hi")
} else {
XCTFail("Decoded value was not found.")
}
} else {
XCTFail("Failed to decode the inbound response.")
}
}
}