Current section

Files

Jump to
codex_sdk lib codex plugins writer.ex
Raw

lib/codex/plugins/writer.ex

defmodule Codex.Plugins.Writer do
@moduledoc false
alias Codex.Plugins.{Errors, Manifest, Marketplace, Paths, Reader}
@type write_manifest_error :: Errors.file_exists_error() | Errors.io_error()
@type add_marketplace_plugin_result :: %{
marketplace_path: String.t(),
marketplace: Marketplace.t(),
plugin_name: String.t()
}
@spec write_manifest(String.t(), Manifest.t(), keyword()) ::
:ok | {:error, write_manifest_error()}
def write_manifest(path, %Manifest{} = manifest, opts \\ [])
when is_binary(path) and is_list(opts) do
manifest_path = Paths.manifest_path(path)
payload = Manifest.to_map(manifest)
with {:ok, json} <- encode_json(payload),
:ok <- prepare_parent(manifest_path, opts) do
write_file(manifest_path, json, opts)
end
end
@spec write_marketplace(String.t(), Marketplace.t(), keyword()) :: :ok | {:error, term()}
def write_marketplace(path, %Marketplace{} = marketplace, opts \\ [])
when is_binary(path) and is_list(opts) do
marketplace_path = Path.expand(path)
with {:ok, _root} <- Paths.marketplace_root(marketplace_path),
{:ok, final_marketplace} <- maybe_merge_marketplace(marketplace_path, marketplace, opts),
:ok <- Reader.validate_marketplace_sources(marketplace_path, final_marketplace),
{:ok, json} <- encode_json(Marketplace.to_map(final_marketplace)),
:ok <- prepare_parent(marketplace_path, opts) do
write_file(
marketplace_path,
json,
Keyword.put(
opts,
:overwrite,
Keyword.get(opts, :overwrite, false) or Keyword.get(opts, :merge, false)
)
)
end
end
@spec add_marketplace_plugin(String.t(), Marketplace.plugin_t(), keyword()) ::
{:ok, add_marketplace_plugin_result()} | {:error, term()}
def add_marketplace_plugin(path, plugin, opts \\ [])
when is_binary(path) and is_map(plugin) and is_list(opts) do
marketplace_path = Path.expand(path)
overwrite? = Keyword.get(opts, :overwrite, false)
with {:ok, _root} <- Paths.marketplace_root(marketplace_path),
{:ok, marketplace} <- load_or_initialize_marketplace(marketplace_path, opts),
{:ok, updated_marketplace} <-
Marketplace.put_plugin(marketplace, plugin, overwrite: overwrite?),
:ok <- Reader.validate_marketplace_sources(marketplace_path, updated_marketplace),
:ok <-
write_marketplace(marketplace_path, updated_marketplace,
create_parents: true,
overwrite: true
) do
{:ok,
%{
marketplace_path: marketplace_path,
marketplace: updated_marketplace,
plugin_name: plugin.name
}}
else
{:error, {:plugin_conflict, %{plugin_name: plugin_name}}} ->
{:error, Errors.plugin_conflict(marketplace_path, plugin_name)}
{:error, _reason} = error ->
error
end
end
@spec encode_json(map()) :: {:ok, String.t()}
def encode_json(map) when is_map(map), do: {:ok, encode_value(map, 0) <> "\n"}
defp maybe_merge_marketplace(path, marketplace, opts) do
if Keyword.get(opts, :merge, false) and File.exists?(path) do
with {:ok, existing} <- Reader.read_marketplace(path),
{:ok, merged} <-
Marketplace.merge(existing, marketplace,
overwrite: Keyword.get(opts, :overwrite, false)
) do
{:ok, merged}
else
{:error, {:plugin_conflict, %{plugin_name: plugin_name}}} ->
{:error, Errors.plugin_conflict(path, plugin_name)}
{:error, _reason} = error ->
error
end
else
{:ok, marketplace}
end
end
defp load_or_initialize_marketplace(path, opts) do
if File.exists?(path) do
Reader.read_marketplace(path)
else
build_default_marketplace(path, opts)
end
end
defp build_default_marketplace(path, opts) do
with {:ok, root} <- Paths.marketplace_root(path) do
Marketplace.parse(%{
"name" => Keyword.get(opts, :marketplace_name) || default_marketplace_name(root),
"interface" => %{
"displayName" =>
Keyword.get(opts, :marketplace_display_name) ||
default_marketplace_display_name(root)
},
"plugins" => []
})
end
end
defp default_marketplace_name(root) do
case Path.basename(root) do
"" -> "local-marketplace"
"/" -> "local-marketplace"
basename -> "#{basename}-local"
end
end
defp default_marketplace_display_name(root) do
case Path.basename(root) do
"" -> "Local Plugins"
"/" -> "Local Plugins"
basename -> "#{humanize_name(basename)} Plugins"
end
end
defp prepare_parent(path, opts) do
if Keyword.get(opts, :create_parents, false) do
case File.mkdir_p(Path.dirname(path)) do
:ok -> :ok
{:error, reason} -> {:error, Errors.io(:mkdir, Path.dirname(path), reason)}
end
else
:ok
end
end
defp write_file(path, contents, opts) do
overwrite? = Keyword.get(opts, :overwrite, false)
if File.exists?(path) and not overwrite? do
{:error, Errors.file_exists(path)}
else
do_write_file(path, contents)
end
end
defp do_write_file(path, contents) do
temp_path = "#{path}.tmp-#{System.unique_integer([:positive])}"
with :ok <- File.write(temp_path, contents),
:ok <- File.rename(temp_path, path) do
:ok
else
{:error, reason} ->
_ = File.rm(temp_path)
{:error, Errors.io(:write, path, reason)}
end
end
defp encode_value(map, indent) when is_map(map) do
if map_size(map) == 0 do
"{}"
else
next_indent = indent + 2
entries =
map
|> Enum.map(fn {key, value} -> {encode_key(key), value} end)
|> Enum.sort_by(&elem(&1, 0))
|> Enum.map(fn {key, value} ->
indent(next_indent) <> Jason.encode!(key) <> ": " <> encode_value(value, next_indent)
end)
"{\n" <> Enum.join(entries, ",\n") <> "\n" <> indent(indent) <> "}"
end
end
defp encode_value(list, indent) when is_list(list) do
if list == [] do
"[]"
else
next_indent = indent + 2
entries =
Enum.map(list, fn value ->
indent(next_indent) <> encode_value(value, next_indent)
end)
"[\n" <> Enum.join(entries, ",\n") <> "\n" <> indent(indent) <> "]"
end
end
defp encode_value(value, _indent) when is_binary(value), do: Jason.encode!(value)
defp encode_value(value, _indent) when is_number(value), do: Jason.encode!(value)
defp encode_value(true, _indent), do: "true"
defp encode_value(false, _indent), do: "false"
defp encode_value(nil, _indent), do: "null"
defp encode_value(value, _indent) when is_atom(value), do: Jason.encode!(Atom.to_string(value))
defp encode_key(key) when is_binary(key), do: key
defp encode_key(key) when is_atom(key), do: Atom.to_string(key)
defp encode_key(key), do: to_string(key)
defp indent(size), do: String.duplicate(" ", size)
defp humanize_name(name) when is_binary(name) do
name
|> String.split(~r/[-_]+/, trim: true)
|> Enum.map_join(" ", &String.capitalize/1)
end
end