Current section

Files

Jump to
cmdc lib cmdc backend results.ex
Raw

lib/cmdc/backend/results.ex

defmodule CMDC.Backend.Results do
# 内部模块 — `CMDC.Backend` 8 个标准 Result struct + `FileData` typed map。
# struct 定义会被 `CMDC.Backend` 公开模块的回调签名引用,但本模块本身不暴露。
@moduledoc false
defmodule FileData do
@moduledoc """
文件数据 typed map。
## 格式版本
- **v1**(legacy):`content` 为 `[binary]`(按 `\\n` 分割的行列表),无 `encoding` 字段
- **v2**(current):`content` 为 `binary`,加 `encoding` 字段(`"utf-8"` 或 `"base64"`)
Backend 接受两种格式作为输入,输出推荐 v2。
"""
@type t :: %{
required(:content) => String.t() | [String.t()],
optional(:encoding) => String.t(),
optional(:created_at) => String.t(),
optional(:modified_at) => String.t()
}
@doc "构建 v2 格式文件数据。"
@spec new(String.t(), keyword()) :: t()
def new(content, opts \\ []) when is_binary(content) do
now = DateTime.utc_now() |> DateTime.to_iso8601()
%{
content: content,
encoding: Keyword.get(opts, :encoding, "utf-8"),
created_at: Keyword.get(opts, :created_at, now),
modified_at: Keyword.get(opts, :modified_at, now)
}
end
@doc "返回 v1(legacy)格式(lines list)。"
@spec to_legacy(t()) :: %{content: [String.t()]}
def to_legacy(%{content: content} = file_data) when is_binary(content) do
lines = String.split(content, "\n")
file_data |> Map.drop([:encoding]) |> Map.put(:content, lines)
end
def to_legacy(%{content: lines} = file_data) when is_list(lines), do: file_data
@doc "从 v1 (lines) 转 v2 (string);幂等。"
@spec to_v2(t()) :: t()
def to_v2(%{content: content} = file_data) when is_list(content) do
file_data
|> Map.put(:content, Enum.join(content, "\n"))
|> Map.put_new(:encoding, "utf-8")
end
def to_v2(file_data), do: file_data
@doc "字符串内容(处理 v1/v2 两种格式)。"
@spec to_string(t()) :: String.t()
def to_string(%{content: content}) when is_binary(content), do: content
def to_string(%{content: lines}) when is_list(lines), do: Enum.join(lines, "\n")
end
defmodule FileInfo do
@moduledoc "目录条目元信息。"
@type t :: %{
required(:path) => String.t(),
optional(:is_dir) => boolean(),
optional(:size) => non_neg_integer(),
optional(:modified_at) => String.t()
}
end
defmodule GrepMatch do
@moduledoc "grep 单条匹配。"
@type t :: %{
required(:path) => String.t(),
required(:line) => pos_integer(),
required(:text) => String.t()
}
end
defmodule ReadResult do
@moduledoc "`backend.read/3` 返回。"
@derive Jason.Encoder
defstruct [:error, :file_data]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
file_data: FileData.t() | nil
}
@doc "成功构造器。"
@spec ok(FileData.t()) :: t()
def ok(file_data), do: %__MODULE__{file_data: file_data}
@doc "失败构造器。"
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule WriteResult do
@moduledoc """
`backend.write/2` 返回。
`files_update` 字段用于 LangGraph state-style backend(如 `State`)返回
状态更新 map;FilesystemBackend / SandboxBackend 已直接写盘则为 `nil`。
"""
@derive Jason.Encoder
defstruct [:error, :path, :files_update]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
path: String.t() | nil,
files_update: map() | nil
}
@spec ok(String.t(), map() | nil) :: t()
def ok(path, files_update \\ nil) do
%__MODULE__{path: path, files_update: files_update}
end
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule EditResult do
@moduledoc "`backend.edit/4` 返回。"
@derive Jason.Encoder
defstruct [:error, :path, :files_update, :occurrences]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
path: String.t() | nil,
files_update: map() | nil,
occurrences: non_neg_integer() | nil
}
@spec ok(String.t(), non_neg_integer(), map() | nil) :: t()
def ok(path, occurrences, files_update \\ nil) do
%__MODULE__{path: path, occurrences: occurrences, files_update: files_update}
end
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule LsResult do
@moduledoc "`backend.ls/1` 返回。"
@derive Jason.Encoder
defstruct [:error, :entries]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
entries: [FileInfo.t()] | nil
}
@spec ok([FileInfo.t()]) :: t()
def ok(entries), do: %__MODULE__{entries: entries}
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule GrepResult do
@moduledoc "`backend.grep/3` 返回。"
@derive Jason.Encoder
defstruct [:error, :matches]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
matches: [GrepMatch.t()] | nil
}
@spec ok([GrepMatch.t()]) :: t()
def ok(matches), do: %__MODULE__{matches: matches}
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule GlobResult do
@moduledoc "`backend.glob/2` 返回。"
@derive Jason.Encoder
defstruct [:error, :matches]
@type t :: %__MODULE__{
error: nil | atom() | String.t(),
matches: [FileInfo.t()] | nil
}
@spec ok([FileInfo.t()]) :: t()
def ok(matches), do: %__MODULE__{matches: matches}
@spec error(atom() | String.t()) :: t()
def error(reason), do: %__MODULE__{error: reason}
end
defmodule FileUploadResponse do
@moduledoc "`backend.upload_files/1` 单文件结果。"
@derive Jason.Encoder
defstruct [:path, :error]
@type t :: %__MODULE__{
path: String.t(),
error: nil | atom() | String.t()
}
@spec ok(String.t()) :: t()
def ok(path), do: %__MODULE__{path: path}
@spec error(String.t(), atom() | String.t()) :: t()
def error(path, reason), do: %__MODULE__{path: path, error: reason}
end
defmodule FileDownloadResponse do
@moduledoc "`backend.download_files/1` 单文件结果。"
@derive Jason.Encoder
defstruct [:path, :content, :error]
@type t :: %__MODULE__{
path: String.t(),
content: binary() | nil,
error: nil | atom() | String.t()
}
@spec ok(String.t(), binary()) :: t()
def ok(path, content), do: %__MODULE__{path: path, content: content}
@spec error(String.t(), atom() | String.t()) :: t()
def error(path, reason), do: %__MODULE__{path: path, error: reason}
end
# ==========================================================================
# 错误码标准
# ==========================================================================
@standard_errors [:file_not_found, :permission_denied, :is_directory, :invalid_path]
@doc """
4 个标准错误码 atom 白名单。
Backend 实现优先映射到这 4 个,无法对应时再用 string。
"""
@spec standard_errors() :: [atom()]
def standard_errors, do: @standard_errors
@doc "判断给定 reason 是否为标准错误码。"
@spec standard_error?(term()) :: boolean()
def standard_error?(reason) when reason in @standard_errors, do: true
def standard_error?(_), do: false
end