defmodule Gnat.Parsec do defstruct partial: nil import NimbleParsec subject = ascii_string([?!..?~], min: 1) length = integer(min: 1) sid = integer(min: 1) whitespace = ascii_char([32, ?\t]) |> times(min: 1) op_msg = ascii_char([?m,?M]) |> ascii_char([?s,?S]) |> ascii_char([?g,?G]) op_err = ascii_char([?-]) |> ascii_char([?e,?E]) |> ascii_char([?r,?R]) |> ascii_char([?r,?R]) op_info = ascii_char([?i,?I]) |> ascii_char([?n,?N]) |> ascii_char([?f,?F]) |> ascii_char([?o,?O]) op_ping = ascii_char([?p,?P]) |> ascii_char([?i,?I]) |> ascii_char([?n,?N]) |> ascii_char([?g,?G]) op_pong = ascii_char([?p,?P]) |> ascii_char([?o,?O]) |> ascii_char([?n,?N]) |> ascii_char([?g,?G]) op_ok = ascii_char([?+]) |> ascii_char([?o,?O]) |> ascii_char([?k,?K]) err = replace(op_err, :err) |> ignore(whitespace) |> ignore(string("'")) |> optional(utf8_string([not: ?'], min: 1)) |> ignore(string("'\r\n")) msg = replace(op_msg, :msg) |> ignore(whitespace) |> concat(subject) |> ignore(whitespace) |> concat(sid) |> ignore(whitespace) |> choice([ subject |> ignore(whitespace) |> concat(length), length ]) |> ignore(string("\r\n")) ok = replace(op_ok |> string("\r\n"), :ok) ping = replace(op_ping |> string("\r\n"), :ping) pong = replace(op_pong |> string("\r\n"), :pong) info = replace(op_info, :info) |> ignore(whitespace) |> utf8_string([not: ?\r], min: 2) |> ignore(string("\r\n")) defparsecp :command, choice([msg, ok, ping, pong, info, err]) def new, do: %__MODULE__{} def parse(%__MODULE__{partial: nil}=state, string) do {partial, commands} = parse_commands(string, []) {%{state | partial: partial}, commands} end def parse(%__MODULE__{partial: partial}=state, string) do {partial, commands} = parse_commands(partial <> string, []) {%{state | partial: partial}, commands} end def parse_commands("", list), do: {nil, Enum.reverse(list)} def parse_commands(str, list) do case parse_command(str) do {:ok, command, rest} -> parse_commands(rest, [command | list]) {:error, partial} -> {partial, Enum.reverse(list)} end end @spec parse_command(binary()) :: {:ok, tuple(), binary()} | {:error, binary()} def parse_command(string) do case command(string) do {:ok, [:msg, subject, sid, length], rest, _, _, _} -> finish_msg(subject, sid, nil, length, rest, string) {:ok, [:msg, subject, sid, reply_to, length], rest, _, _, _} -> finish_msg(subject, sid, reply_to, length, rest, string) {:ok, [atom], rest, _, _, _} -> {:ok, atom, rest} {:ok, [:info, json], rest, _, _, _} -> {:ok, {:info, Jason.decode!(json, keys: :atoms)}, rest} {:ok, [:err, msg], rest, _, _, _} -> {:ok, {:error, msg}, rest} {:error, _, _, _, _, _} -> {:error, string} end end def finish_msg(subject, sid, reply_to, length, rest, string) do case rest do << body::size(length)-binary, "\r\n", rest::binary>> -> {:ok, {:msg, subject, sid, reply_to, body}, rest} _other -> {:error, string} end end end