Current section
Files
Jump to
Current section
Files
lib/nexus/parser.ex
defmodule Nexus.Parser do
@moduledoc "Should parse the command and return the value"
import Nexus.Parser.DSL
alias Nexus.Command
alias Nexus.Command.Input
alias Nexus.FailedCommandParsing, as: Error
@spec run!(binary, Command.t()) :: Input.t()
def run!(raw, %Command{} = cmd) do
raw
|> String.trim_trailing()
|> String.trim_leading()
|> parse_command(cmd)
|> case do
{:ok, input} ->
input
{:error, _} ->
raise Error, "Failed to parse command #{inspect(cmd)}"
:error ->
raise Error,
"Failed to parse command #{inspect(cmd)} with subcommands #{inspect(cmd.subcommands)}"
end
end
defp parse_subcommand(input, cmd) do
case parse_command(input, cmd) do
{:ok, input} -> {:halt, {:ok, Map.put(input, :subcommand, cmd.name)}}
{:error, _} -> {:cont, :error}
end
end
defp parse_command(input, %Command{type: :null, subcommands: [_ | _] = subs} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name) do
Enum.reduce_while(subs, :error, fn sub, _acc -> parse_subcommand(rest, sub) end)
end
end
defp parse_command(input, %Command{type: :null} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name) do
{:ok, Input.parse!(nil, rest)}
end
end
defp parse_command(input, %Command{type: :string} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name),
{:ok, value} <- maybe_parse_required(cmd, fn -> string(rest) end) do
{:ok, Input.parse!(value, input)}
end
end
defp parse_command(input, %Command{type: :integer} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name),
{:ok, value} <- maybe_parse_required(cmd, fn -> integer(rest) end) do
{:ok,
value
|> string_to!(:integer)
|> Input.parse!(input)}
end
end
defp parse_command(input, %Command{type: :float} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name),
{:ok, value} <- maybe_parse_required(cmd, fn -> float(rest) end) do
{:ok,
value
|> string_to!(:float)
|> Input.parse!(input)}
end
end
defp parse_command(input, %Command{type: :atom} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name),
{:ok, value} <- maybe_parse_required(cmd, fn -> string(rest) end) do
{:ok,
value
|> string_to!(:atom)
|> Input.parse!(input)}
end
end
defp parse_command(input, %Command{type: {:enum, values}} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name),
{:ok, value} <- maybe_parse_required(cmd, fn -> enum(rest, values) end) do
value =
cond do
is_binary(hd(values)) -> value
is_atom(value) -> value
true -> string_to!(value, :atom)
end
{:ok, Input.parse!(value, input)}
end
end
@spec string_to!(binary, atom) :: term
defp string_to!(raw, :integer) do
case Integer.parse(raw) do
{int, ""} -> int
_ -> raise Error, "#{raw} is not a valid integer"
end
end
defp string_to!(raw, :float) do
case Float.parse(raw) do
{float, ""} -> float
_ -> raise Error, "#{raw} is not a valid float"
end
end
# final user shoul not be use very often
defp string_to!(raw, :atom) do
String.to_atom(raw)
end
@spec maybe_parse_required(Command.t(), (() -> {:ok, {binary, binary} | {:error, term}})) ::
{:ok, term} | {:error, term}
defp maybe_parse_required(%Command{required: true}, fun) do
with {:ok, {value, _}} <- fun.() do
{:ok, value}
end
end
defp maybe_parse_required(%Command{required: false} = cmd, fun) do
case fun.() do
{:ok, {value, _}} -> {:ok, value}
{:error, _} -> {:ok, cmd.default}
end
end
end