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