Current section

Files

Jump to
snakebridge lib snakebridge helpers.ex
Raw

lib/snakebridge/helpers.ex

defmodule SnakeBridge.Helpers do
@moduledoc """
Helper registry discovery and configuration for SnakeBridge.
"""
alias SnakeBridge.{Config, HelperRegistryError}
@type helper_info :: map()
@spec discover() :: {:ok, [helper_info()]} | {:error, term()}
def discover do
discover(runtime_config())
end
@spec discover(Config.t() | map()) :: {:ok, [helper_info()]} | {:error, term()}
def discover(%Config{} = config) do
discover(config_to_map(config))
end
def discover(%{} = config) do
payload = payload_config(config, include_adapter_root: true)
case python_runner().run(helper_index_script(), [Jason.encode!(payload)], runner_opts()) do
{:ok, output} ->
parse_output(output)
{:error, {:python_exit, _status, output}} ->
{:error, HelperRegistryError.from_python_output(output)}
{:error, reason} ->
{:error, reason}
end
end
@spec runtime_config() :: map()
def runtime_config do
%{
helper_paths: Application.get_env(:snakebridge, :helper_paths, ["priv/python/helpers"]),
helper_pack_enabled: Application.get_env(:snakebridge, :helper_pack_enabled, true),
helper_allowlist: Application.get_env(:snakebridge, :helper_allowlist, :all),
inline_enabled: Application.get_env(:snakebridge, :inline_enabled, false)
}
end
@spec enabled?(map()) :: boolean()
def enabled?(%{} = config) do
normalized = normalize_config(config)
normalized.helper_allowlist != [] and
(normalized.helper_pack_enabled == true or normalized.helper_paths != [])
end
@spec payload_config(map(), keyword()) :: map()
def payload_config(%{} = config, opts \\ []) do
normalized = normalize_config(config)
payload = %{
"helper_paths" => normalized.helper_paths,
"helper_pack_enabled" => normalized.helper_pack_enabled,
"helper_allowlist" => allowlist_payload(normalized.helper_allowlist)
}
if Keyword.get(opts, :include_adapter_root, false) do
Map.put(payload, "adapter_root", adapter_root())
else
payload
end
end
defp config_to_map(%Config{} = config) do
%{
helper_paths: config.helper_paths,
helper_pack_enabled: config.helper_pack_enabled,
helper_allowlist: config.helper_allowlist,
inline_enabled: config.inline_enabled
}
end
defp normalize_config(%{} = config) do
%{
helper_paths: normalize_paths(Map.get(config, :helper_paths, ["priv/python/helpers"])),
helper_pack_enabled: Map.get(config, :helper_pack_enabled, true) == true,
helper_allowlist: normalize_allowlist(Map.get(config, :helper_allowlist, :all)),
inline_enabled: Map.get(config, :inline_enabled, false) == true
}
end
defp normalize_paths(paths) do
paths
|> List.wrap()
|> Enum.map(&to_string/1)
|> Enum.map(&Path.expand/1)
|> Enum.uniq()
end
defp normalize_allowlist(:all), do: :all
defp normalize_allowlist("all"), do: :all
defp normalize_allowlist(nil), do: :all
defp normalize_allowlist(:none), do: []
defp normalize_allowlist(list) when is_list(list) do
list
|> Enum.map(&to_string/1)
|> Enum.uniq()
end
defp normalize_allowlist(other), do: [to_string(other)]
defp allowlist_payload(:all), do: "all"
defp allowlist_payload(list) when is_list(list), do: list
defp adapter_root do
:snakebridge
|> :code.priv_dir()
|> to_string()
|> Path.join("python")
end
defp python_runner do
Application.get_env(:snakebridge, :python_runner, SnakeBridge.PythonRunner.System)
end
defp runner_opts do
config = Application.get_env(:snakebridge, :helper_registry, [])
Keyword.take(config, [:timeout, :env, :cd])
end
defp parse_output(output) do
case Jason.decode(output) do
{:ok, results} when is_list(results) -> {:ok, results}
{:ok, %{"error" => error}} -> {:error, error}
{:error, _} -> {:error, {:json_parse, output}}
end
end
defp helper_index_script do
~S"""
import json
import sys
config = json.loads(sys.argv[1])
adapter_root = config.pop("adapter_root", None)
if adapter_root:
sys.path.insert(0, adapter_root)
from snakebridge_adapter import helper_registry_index
index = helper_registry_index(config)
print(json.dumps(index))
"""
end
end