Packages
execution_plane_process
0.1.0
Execution Plane process launch, stdio, PTY, and process-session runtime.
Current section
Files
Jump to
Current section
Files
lib/execution_plane/command.ex
defmodule ExecutionPlane.Command do
@moduledoc """
Generic transport-level command invocation.
This struct stays below CLI-domain concerns. It only carries the executable,
argv, and launch-time process options needed by the execution-surface
substrate.
"""
@enforce_keys [:command]
defstruct command: nil, args: [], cwd: nil, env: %{}, clear_env?: false, user: nil
@type env_key :: String.t()
@type env_value :: String.t()
@type env_map :: %{optional(env_key()) => env_value()}
@type user :: String.t() | nil
@type t :: %__MODULE__{
command: String.t(),
args: [String.t()],
cwd: String.t() | nil,
env: env_map(),
clear_env?: boolean(),
user: user()
}
@doc """
Builds a normalized transport command.
"""
@spec new(String.t(), [String.t()] | keyword(), keyword()) :: t()
def new(command, args \\ [], opts \\ [])
when is_binary(command) and is_list(args) and is_list(opts) do
{args, opts} =
if opts == [] and keyword_list?(args) do
{[], args}
else
{args, opts}
end
%__MODULE__{
command: command,
args: args,
cwd: Keyword.get(opts, :cwd),
env: normalize_env(Keyword.get(opts, :env, %{})),
clear_env?: Keyword.get(opts, :clear_env?, false),
user: normalize_user(Keyword.get(opts, :user))
}
end
@doc """
Returns the executable and argv as a flat list.
"""
@spec argv(t()) :: [String.t()]
def argv(%__MODULE__{} = command) do
[command.command | command.args]
end
@doc """
Adds or replaces one environment variable.
"""
@spec put_env(t(), String.t() | atom(), String.t() | atom() | number() | boolean()) :: t()
def put_env(%__MODULE__{} = command, key, value) do
merge_env(command, %{normalize_env_key(key) => normalize_env_value(value)})
end
@doc """
Merges environment variables into the invocation.
"""
@spec merge_env(t(), map()) :: t()
def merge_env(%__MODULE__{} = command, env) when is_map(env) do
%{command | env: Map.merge(command.env, normalize_env(env))}
end
@doc """
Validates the generic transport command contract.
"""
@spec validate(t()) ::
:ok
| {:error, {:invalid_command, term()}}
| {:error, {:invalid_args, term()}}
| {:error, {:invalid_cwd, term()}}
| {:error, {:invalid_env, term()}}
| {:error, {:invalid_clear_env, term()}}
| {:error, {:invalid_user, term()}}
def validate(%__MODULE__{
command: command,
args: args,
cwd: cwd,
env: env,
clear_env?: clear_env?,
user: user
}) do
validators = [
fn -> validate_command(command) end,
fn -> validate_args(args) end,
fn -> validate_cwd(cwd) end,
fn -> validate_env(env) end,
fn -> validate_clear_env(clear_env?) end,
fn -> validate_user(user) end
]
Enum.reduce_while(validators, :ok, fn validator, :ok ->
case validator.() do
:ok -> {:cont, :ok}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp normalize_env(env) when is_map(env) do
Map.new(env, fn {key, value} ->
{normalize_env_key(key), normalize_env_value(value)}
end)
end
defp normalize_env(_other), do: %{}
defp normalize_env_key(key) when is_binary(key), do: key
defp normalize_env_key(key) when is_atom(key), do: Atom.to_string(key)
defp normalize_env_key(key), do: to_string(key)
defp normalize_env_value(value) when is_binary(value), do: value
defp normalize_env_value(value) when is_atom(value), do: Atom.to_string(value)
defp normalize_env_value(value) when is_boolean(value), do: to_string(value)
defp normalize_env_value(value) when is_integer(value), do: Integer.to_string(value)
defp normalize_env_value(value) when is_float(value), do: Float.to_string(value)
defp validate_command(command) when is_binary(command) and command != "", do: :ok
defp validate_command(command), do: {:error, {:invalid_command, command}}
defp validate_args(args) when is_list(args) do
if Enum.all?(args, &is_binary/1) do
:ok
else
{:error, {:invalid_args, args}}
end
end
defp validate_args(args), do: {:error, {:invalid_args, args}}
defp validate_cwd(nil), do: :ok
defp validate_cwd(cwd) when is_binary(cwd), do: :ok
defp validate_cwd(cwd), do: {:error, {:invalid_cwd, cwd}}
defp validate_env(env) when is_map(env) do
if Enum.all?(env, fn {key, value} -> is_binary(key) and is_binary(value) end) do
:ok
else
{:error, {:invalid_env, env}}
end
end
defp validate_env(env), do: {:error, {:invalid_env, env}}
defp validate_clear_env(value) when is_boolean(value), do: :ok
defp validate_clear_env(value), do: {:error, {:invalid_clear_env, value}}
defp validate_user(nil), do: :ok
defp validate_user(user) when is_binary(user) and user != "", do: :ok
defp validate_user(user), do: {:error, {:invalid_user, user}}
defp normalize_user(nil), do: nil
defp normalize_user(user) when is_binary(user), do: user
defp normalize_user(user) when is_atom(user), do: Atom.to_string(user)
defp normalize_user(user), do: to_string(user)
defp keyword_list?(list) when is_list(list), do: Keyword.keyword?(list)
defp keyword_list?(_other), do: false
end