feat(command): add stream argument parsing and reply builders
This commit is contained in:
1 parent
18f8f68088
commit
a6b04f9829
1 file changed
+397
@@ -373,3 +373,400 @@ defmodule Excache.Command.Streams do
|
||||
|
||||
defp parseAddOptions([], _key, _nomkstream, _trim), do: :error
|
||||
|
||||
defp parseTrim([]), do: {:ok, {nil, []}}
|
||||
|
||||
defp parseTrim([token | rest]) do
|
||||
case String.upcase(token) do
|
||||
"MAXLEN" ->
|
||||
case parseTrimBody(rest, :maxlen) do
|
||||
{:ok, parsed, remaining} -> {:ok, {parsed, remaining}}
|
||||
:error -> :error
|
||||
end
|
||||
|
||||
"MINID" ->
|
||||
case parseTrimBody(rest, :minid) do
|
||||
{:ok, parsed, remaining} -> {:ok, {parsed, remaining}}
|
||||
:error -> :error
|
||||
end
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseTrimBody(tokens, kind) do
|
||||
tokens = dropApproxMarker(tokens)
|
||||
|
||||
case tokens do
|
||||
[threshold | rest] ->
|
||||
rest = dropLimit(rest)
|
||||
|
||||
case buildTrim(kind, threshold) do
|
||||
{:ok, parsed} -> {:ok, parsed, rest}
|
||||
:error -> :error
|
||||
end
|
||||
|
||||
[] ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp dropApproxMarker([marker | rest]) when marker in ["~", "="], do: rest
|
||||
defp dropApproxMarker(tokens), do: tokens
|
||||
|
||||
defp dropLimit(["LIMIT", _value | rest]), do: rest
|
||||
defp dropLimit(["limit", _value | rest]), do: rest
|
||||
defp dropLimit(tokens), do: tokens
|
||||
|
||||
defp buildTrim(:maxlen, threshold) do
|
||||
case Integer.parse(threshold) do
|
||||
{value, ""} when value >= 0 -> {:ok, {:maxlen, value}}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp buildTrim(:minid, threshold) do
|
||||
case parseThresholdId(threshold) do
|
||||
{:ok, id} -> {:ok, {:minid, id}}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp trimTokens(nil), do: []
|
||||
defp trimTokens({:maxlen, count}), do: ["MAXLEN", Integer.to_string(count)]
|
||||
defp trimTokens({:minid, {ms, seq}}), do: ["MINID", "#{ms}-#{seq}"]
|
||||
|
||||
defp parseRead(args) do
|
||||
{count, rest} = takeReadCount(args, :all)
|
||||
|
||||
case splitStreams(rest) do
|
||||
{:ok, keys, ids} when length(keys) == length(ids) and keys != [] ->
|
||||
{:ok, count, Enum.zip(keys, ids)}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseReadGroup(["GROUP", group, consumer | rest]) do
|
||||
{count, rest} = takeReadCount(rest, :all)
|
||||
{noack, rest} = takeNoack(rest)
|
||||
|
||||
case splitStreams(rest) do
|
||||
{:ok, keys, ids} when length(keys) == length(ids) and keys != [] ->
|
||||
{:ok, group, consumer, count, noack, Enum.zip(keys, ids)}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseReadGroup(_args), do: :error
|
||||
|
||||
defp takeReadCount(["COUNT", raw | rest], _default) do
|
||||
case Integer.parse(raw) do
|
||||
{value, ""} when value >= 0 -> {value, rest}
|
||||
_ -> {:all, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp takeReadCount([token, raw | rest], default) when token in ["count"] do
|
||||
takeReadCount(["COUNT", raw | rest], default)
|
||||
end
|
||||
|
||||
defp takeReadCount(rest, default), do: {default, rest}
|
||||
|
||||
defp takeNoack(["NOACK" | rest]), do: {true, rest}
|
||||
defp takeNoack(["noack" | rest]), do: {true, rest}
|
||||
defp takeNoack(rest), do: {false, rest}
|
||||
|
||||
defp splitStreams(tokens) do
|
||||
case Enum.split_while(tokens, &(String.upcase(&1) != "STREAMS")) do
|
||||
{leading, ["STREAMS" | rest]} ->
|
||||
# any leading BLOCK ms is tolerated and ignored here
|
||||
_ = leading
|
||||
half = div(length(rest), 2)
|
||||
|
||||
if rem(length(rest), 2) == 0 and half > 0 do
|
||||
{:ok, Enum.take(rest, half), Enum.drop(rest, half)}
|
||||
else
|
||||
:error
|
||||
end
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp resolveReadId(key, "$"), do: lastIdOf(key)
|
||||
defp resolveReadId(_key, "+"), do: {maxMs(), maxSeq()}
|
||||
|
||||
defp resolveReadId(_key, raw) do
|
||||
case parseThresholdId(raw) do
|
||||
{:ok, id} -> id
|
||||
:error -> {0, 0}
|
||||
end
|
||||
end
|
||||
|
||||
defp lastIdOf(key) do
|
||||
case Store.xinfoStream(key) do
|
||||
{:ok, info} -> info.lastGeneratedId
|
||||
_ -> {0, 0}
|
||||
end
|
||||
end
|
||||
|
||||
defp parseGroupStart("$"), do: {:ok, :last}
|
||||
defp parseGroupStart("0"), do: {:ok, {:id, {0, 0}}}
|
||||
|
||||
defp parseGroupStart(raw) do
|
||||
case parseThresholdId(raw) do
|
||||
{:ok, id} -> {:ok, {:id, id}}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parsePendingRange(["IDLE", idleRaw | rest]) do
|
||||
case Integer.parse(idleRaw) do
|
||||
{idle, ""} -> withPendingCore(rest, idle)
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parsePendingRange(rest), do: withPendingCore(rest, nil)
|
||||
|
||||
defp withPendingCore([startRaw, endRaw, countRaw | rest], minIdle) do
|
||||
case Integer.parse(countRaw) do
|
||||
{count, ""} when count >= 0 ->
|
||||
consumer = List.first(rest)
|
||||
{:ok, startRaw, endRaw, count, consumer, minIdle}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp withPendingCore(_rest, _minIdle), do: :error
|
||||
|
||||
defp pendingBound("-", _default), do: {:ok, {0, 0}}
|
||||
defp pendingBound("+", _default), do: {:ok, {maxMs(), maxSeq()}}
|
||||
|
||||
defp pendingBound(raw, _default) do
|
||||
case parseThresholdId(raw) do
|
||||
{:ok, id} -> {:ok, id}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseClaim(tokens), do: parseClaimIds(tokens, [])
|
||||
|
||||
defp parseClaimIds([token | rest], acc) do
|
||||
case parseThresholdId(token) do
|
||||
{:ok, id} ->
|
||||
parseClaimIds(rest, [id | acc])
|
||||
|
||||
:error ->
|
||||
case acc do
|
||||
[] -> :error
|
||||
_ -> parseClaimOpts([token | rest], Enum.reverse(acc), defaultClaimOpts())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parseClaimIds([], acc) when acc != [], do: {:ok, Enum.reverse(acc), defaultClaimOpts()}
|
||||
defp parseClaimIds([], _acc), do: :error
|
||||
|
||||
defp parseClaimOpts([], ids, opts), do: {:ok, ids, opts}
|
||||
|
||||
defp parseClaimOpts([token | rest], ids, opts) do
|
||||
case String.upcase(token) do
|
||||
"IDLE" -> claimIntOpt(rest, ids, opts, :idle)
|
||||
"TIME" -> claimIntOpt(rest, ids, opts, :time)
|
||||
"RETRYCOUNT" -> claimIntOpt(rest, ids, opts, :retryCount)
|
||||
"FORCE" -> parseClaimOpts(rest, ids, %{opts | force: true})
|
||||
"JUSTID" -> parseClaimOpts(rest, ids, %{opts | justId: true})
|
||||
"LASTID" -> parseClaimOpts(tl(rest), ids, opts)
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp claimIntOpt([raw | rest], ids, opts, field) do
|
||||
case Integer.parse(raw) do
|
||||
{value, ""} -> parseClaimOpts(rest, ids, Map.put(opts, field, value))
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp claimIntOpt([], _ids, _opts, _field), do: :error
|
||||
|
||||
defp defaultClaimOpts,
|
||||
do: %{idle: nil, time: nil, retryCount: nil, force: false, justId: false}
|
||||
|
||||
defp parseAutoclaim(["COUNT", raw | rest]) do
|
||||
case Integer.parse(raw) do
|
||||
{value, ""} when value > 0 -> finishAutoclaim(rest, value)
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseAutoclaim(rest), do: finishAutoclaim(rest, 100)
|
||||
|
||||
defp finishAutoclaim([], count), do: {:ok, count, false}
|
||||
defp finishAutoclaim(["JUSTID"], count), do: {:ok, count, true}
|
||||
defp finishAutoclaim(["justid"], count), do: {:ok, count, true}
|
||||
defp finishAutoclaim(_rest, _count), do: :error
|
||||
|
||||
defp parseIds(idsRaw) do
|
||||
Enum.reduce_while(idsRaw, {:ok, []}, fn raw, {:ok, acc} ->
|
||||
case parseThresholdId(raw) do
|
||||
{:ok, id} -> {:cont, {:ok, [id | acc]}}
|
||||
:error -> {:halt, :error}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, ids} -> {:ok, Enum.reverse(ids)}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parseThresholdId(raw) do
|
||||
case String.split(raw, "-", parts: 2) do
|
||||
[ms] -> withMs(ms, 0)
|
||||
[ms, seq] -> withMsSeq(ms, seq)
|
||||
end
|
||||
end
|
||||
|
||||
defp parseThresholdBang(raw) do
|
||||
case parseThresholdId(raw) do
|
||||
{:ok, id} -> id
|
||||
:error -> {0, 0}
|
||||
end
|
||||
end
|
||||
|
||||
defp withMs(ms, seq) do
|
||||
case Integer.parse(ms) do
|
||||
{value, ""} when value >= 0 -> {:ok, {value, seq}}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp withMsSeq(ms, seq) do
|
||||
with {msValue, ""} <- Integer.parse(ms),
|
||||
{seqValue, ""} <- Integer.parse(seq),
|
||||
true <- msValue >= 0 and seqValue >= 0 do
|
||||
{:ok, {msValue, seqValue}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp entryReply({id, fields}) do
|
||||
{:array, [{:bulk, Stream.formatId(id)}, fieldsReply(fields)]}
|
||||
end
|
||||
|
||||
defp fieldsReply(nil), do: {:array, nil}
|
||||
defp fieldsReply(fields), do: {:array, Enum.map(fields, &{:bulk, &1})}
|
||||
|
||||
defp streamEntriesReply(key, entries) do
|
||||
{:array, [{:bulk, key}, {:array, Enum.map(entries, &entryReply/1)}]}
|
||||
end
|
||||
|
||||
defp infoReply(info) do
|
||||
[
|
||||
{:bulk, "length"},
|
||||
{:integer, info.length},
|
||||
{:bulk, "last-generated-id"},
|
||||
{:bulk, Stream.formatId(info.lastGeneratedId)},
|
||||
{:bulk, "max-deleted-entry-id"},
|
||||
{:bulk, Stream.formatId(info.maxDeletedId)},
|
||||
{:bulk, "entries-added"},
|
||||
{:integer, info.entriesAdded},
|
||||
{:bulk, "groups"},
|
||||
{:integer, info.groups},
|
||||
{:bulk, "first-entry"},
|
||||
firstOrNil(info.firstEntry),
|
||||
{:bulk, "last-entry"},
|
||||
firstOrNil(info.lastEntry)
|
||||
]
|
||||
end
|
||||
|
||||
defp firstOrNil(nil), do: {:array, nil}
|
||||
defp firstOrNil(entry), do: entryReply(entry)
|
||||
|
||||
defp pendingSummaryReply({0, _minId, _maxId, _perConsumer}) do
|
||||
{:array, [{:integer, 0}, {:bulk, nil}, {:bulk, nil}, {:array, nil}]}
|
||||
end
|
||||
|
||||
defp pendingSummaryReply({total, minId, maxId, perConsumer}) do
|
||||
consumers =
|
||||
Enum.map(perConsumer, fn {consumer, count} ->
|
||||
{:array, [{:bulk, consumer}, {:bulk, Integer.to_string(count)}]}
|
||||
end)
|
||||
|
||||
{:array,
|
||||
[
|
||||
{:integer, total},
|
||||
{:bulk, Stream.formatId(minId)},
|
||||
{:bulk, Stream.formatId(maxId)},
|
||||
{:array, consumers}
|
||||
]}
|
||||
end
|
||||
|
||||
defp pendingRowReply({id, consumer, idle, deliveryCount}) do
|
||||
{:array,
|
||||
[
|
||||
{:bulk, Stream.formatId(id)},
|
||||
{:bulk, consumer},
|
||||
{:integer, idle},
|
||||
{:integer, deliveryCount}
|
||||
]}
|
||||
end
|
||||
|
||||
defp claimReply(results, true) do
|
||||
{:array, Enum.map(results, fn {id, _fields} -> {:bulk, Stream.formatId(id)} end)}
|
||||
end
|
||||
|
||||
defp claimReply(results, false) do
|
||||
{:array, Enum.map(results, &entryReply/1)}
|
||||
end
|
||||
|
||||
defp autoclaimReply(cursor, claimed, deleted, justId) do
|
||||
entriesReply =
|
||||
if justId do
|
||||
{:array, Enum.map(claimed, fn {id, _fields} -> {:bulk, Stream.formatId(id)} end)}
|
||||
else
|
||||
{:array, Enum.map(claimed, &entryReply/1)}
|
||||
end
|
||||
|
||||
deletedReply = {:array, Enum.map(deleted, fn id -> {:bulk, Stream.formatId(id)} end)}
|
||||
{:array, [{:bulk, Stream.formatId(cursor)}, entriesReply, deletedReply]}
|
||||
end
|
||||
|
||||
defp reply(term, state), do: {[term], state}
|
||||
|
||||
defp orZero({:ok, value}), do: value
|
||||
defp orZero({:error, _}), do: 0
|
||||
|
||||
defp streamError(:wrongtype), do: Reply.wrongtype()
|
||||
defp streamError(:smaller),
|
||||
do: {:error, "ERR The ID specified in XADD is equal or smaller than the target stream top item"}
|
||||
|
||||
defp streamError(:zero), do: {:error, "ERR The ID specified in XADD must be greater than 0-0"}
|
||||
defp streamError(other), do: {:error, "ERR #{other}"}
|
||||
|
||||
defp nogroup(key, group) do
|
||||
{:error,
|
||||
"NOGROUP No such key '#{key}' or consumer group '#{group}' in XREADGROUP with GROUP option"}
|
||||
end
|
||||
|
||||
defp noKeyGroupMessage do
|
||||
"ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically."
|
||||
end
|
||||
|
||||
defp setidSmallerMessage do
|
||||
"ERR The ID specified in XSETID is smaller than the target stream top item"
|
||||
end
|
||||
|
||||
defp now, do: System.system_time(:millisecond)
|
||||
defp maxMs, do: 0xFFFFFFFFFFFFFFFF
|
||||
defp maxSeq, do: 0xFFFFFFFFFFFFFFFF
|
||||
end
|
||||
Reference in new issue
Block a user