Skip to content

Instantly share code, notes, and snippets.

@alco
Created April 28, 2014 15:21
Show Gist options
  • Save alco/11375287 to your computer and use it in GitHub Desktop.
Save alco/11375287 to your computer and use it in GitHub Desktop.
defmodule Memcachedx.Packet.Parser do
@doc """
Parses the response to find the status code and return a friendly message
based on it.
"""
# Those seem to be unused
#alias Memcachedx.Packet.Response.Header, as: Header
#alias Memcachedx.Packet.Response.Body, as: Body
# 1. Organize the code in a top-down approach:
# - public functions go first
# - helpers functions are defined after the point they are first used
def parse_response(message) do
Enum.reverse(recur_response(message, []))
end
# 2. Define helpers functions and macros for repetitive stuff
defmacrop size_bytes(n), do: quote(do: [size(unquote(n)), unit(8)])
defp recur_response("", result), do: result
defp recur_response(message, acc) do
<<
_magic :: size_bytes(1),
opcode :: size_bytes(1),
key_length :: size_bytes(2),
extras_length :: size_bytes(1),
_data_type :: size_bytes(1),
status :: size_bytes(2),
rest :: binary
>> = message
# 3. Don't put many conditionals in one function. Split into smaller
# functions to make it read like prose
intermediate_params = [
opcode: symbolic_opcode(opcode),
key_length: key_length,
extras_length: extras_length,
]
{params, rest} = extract_params(rest, intermediate_params)
# 4. Tail recursion not only reduces memory footprint,
# it also helps to avoid conditionals inside function body
recur_response(rest, [{symbolic_status(status), params}] ++ acc)
end
defp extract_params("", params), do: {params, ""}
defp extract_params(bin, params) do
<<
total_body_length :: size_bytes(4),
opaque :: size_bytes(4),
cas :: size_bytes(8),
rest :: binary
>> = bin
extras_length = params[:extras_length]
body_length = total_body_length - extras_length
<<
extras :: size_bytes(extras_length),
body :: [binary, size(body_length)],
rest :: binary
>> = rest
prefinal_params = params ++ [
total_body_length: total_body_length,
opaque: opaque,
cas: cas,
extras: extras
]
{parse_body_params(prefinal_params, body), rest}
end
defp parse_body_params(params, body) do
key_length = params[:key_length]
# 5. Put conditional assignment into one expression to make it clear
# that the variable will get one of two values exclusively
# Solves problem with getk/getkq having the wrong total_body_length as the
# total_body_length should be (extras_length + key_length + value_length)
value_length = if params[:opcode] == :getk do
params[:total_body_length] - params[:extras_length]
else
params[:total_body_length] - params[:extras_length] - key_length
end
# 6. Again, remove conditions by adding functions
{key, rest_body} = extract_key(key_length, body)
{value, _} = extract_value(value_length, rest_body)
params ++ [key: key, value: value]
end
defp extract_key(0, body), do: {"", body}
defp extract_key(length, body) do
<<
key :: [binary, size(length)],
rest :: binary
>> = body
{key, rest}
end
defp extract_value(0, body), do: {"", body}
defp extract_value(length, body) do
<<
value :: [binary, size(length)],
rest :: binary
>> = body
{value, rest}
end
defp symbolic_status(0), do: :ok
defp symbolic_status(_), do: :error
defp symbolic_opcode(opcode) do
case opcode do
0x00 -> :get
0x01 -> :set
0x02 -> :add
0x03 -> :replace
0x04 -> :delete
0x05 -> :incr
0x06 -> :decr
0x07 -> :quit
0x08 -> :flush
0x0A -> :noop
0x0B -> :version
0x0D -> :getkq
0x0E -> :append
0x0F -> :prepend
0x10 -> :stat
0x11 -> :setq
0x12 -> :addq
0x13 -> :replaceq
0x14 -> :deleteq
0x15 -> :incrq
0x16 -> :decrq
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment