use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::WebFetchPresentation;
pub const MAX_CODE_CHANGE_PATH_BYTES: usize = 4_096;
pub const MAX_CODE_CHANGE_HUNKS: usize = 32;
pub const MAX_CODE_CHANGE_LINES_PER_HUNK: usize = 128;
pub const MAX_CODE_CHANGE_LINES: usize = 1_024;
pub const MAX_CODE_CHANGE_LINE_BYTES: usize = 1_024;
pub const MAX_CODE_CHANGE_PATCH_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum ToolPresentation {
CodeChange(CodeChange),
WebFetch(Box<WebFetchPresentation>),
}
impl ToolPresentation {
#[must_use]
pub const fn code_change(&self) -> Option<&CodeChange> {
match self {
Self::CodeChange(change) => Some(change),
Self::WebFetch(_) => None,
}
}
#[must_use]
pub fn web_fetch(&self) -> Option<&WebFetchPresentation> {
match self {
Self::CodeChange(_) => None,
Self::WebFetch(fetch) => Some(fetch.as_ref()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeChangeKind {
Create,
Update,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeChangeTruncation {
Hunks,
Lines,
LineBytes,
PatchBytes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeChangeLineKind {
Context,
Addition,
Deletion,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CodeChangeLine {
kind: CodeChangeLineKind,
old_line: Option<u32>,
new_line: Option<u32>,
text: String,
}
impl CodeChangeLine {
pub fn new(
kind: CodeChangeLineKind,
old_line: Option<u32>,
new_line: Option<u32>,
text: impl Into<String>,
) -> Result<Self, CodeChangeValidationError> {
let line = Self {
kind,
old_line,
new_line,
text: text.into(),
};
line.validate()?;
Ok(line)
}
#[must_use]
pub const fn kind(&self) -> CodeChangeLineKind {
self.kind
}
#[must_use]
pub const fn old_line(&self) -> Option<u32> {
self.old_line
}
#[must_use]
pub const fn new_line(&self) -> Option<u32> {
self.new_line
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
fn validate(&self) -> Result<(), CodeChangeValidationError> {
if self.text.len() > MAX_CODE_CHANGE_LINE_BYTES {
return Err(CodeChangeValidationError::LineTooLong);
}
let positive = |line: Option<u32>| line.is_some_and(|line| line > 0);
let valid_numbers = match self.kind {
CodeChangeLineKind::Context => positive(self.old_line) && positive(self.new_line),
CodeChangeLineKind::Addition => self.old_line.is_none() && positive(self.new_line),
CodeChangeLineKind::Deletion => positive(self.old_line) && self.new_line.is_none(),
};
if valid_numbers {
Ok(())
} else {
Err(CodeChangeValidationError::InvalidLineNumbers)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CodeChangeHunk {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
lines: Vec<CodeChangeLine>,
}
impl CodeChangeHunk {
pub fn new(
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
lines: Vec<CodeChangeLine>,
) -> Result<Self, CodeChangeValidationError> {
let hunk = Self {
old_start,
old_lines,
new_start,
new_lines,
lines,
};
hunk.validate()?;
Ok(hunk)
}
#[must_use]
pub const fn old_start(&self) -> u32 {
self.old_start
}
#[must_use]
pub const fn old_lines(&self) -> u32 {
self.old_lines
}
#[must_use]
pub const fn new_start(&self) -> u32 {
self.new_start
}
#[must_use]
pub const fn new_lines(&self) -> u32 {
self.new_lines
}
#[must_use]
pub fn lines(&self) -> &[CodeChangeLine] {
&self.lines
}
fn validate(&self) -> Result<(), CodeChangeValidationError> {
if self.lines.is_empty() || self.lines.len() > MAX_CODE_CHANGE_LINES_PER_HUNK {
return Err(CodeChangeValidationError::InvalidHunkLines);
}
for line in &self.lines {
line.validate()?;
}
let old_start = self
.lines
.iter()
.find_map(CodeChangeLine::old_line)
.unwrap_or(0);
let new_start = self
.lines
.iter()
.find_map(CodeChangeLine::new_line)
.unwrap_or(0);
let old_lines = u32::try_from(
self.lines
.iter()
.filter(|line| line.old_line().is_some())
.count(),
)
.unwrap_or(u32::MAX);
let new_lines = u32::try_from(
self.lines
.iter()
.filter(|line| line.new_line().is_some())
.count(),
)
.unwrap_or(u32::MAX);
if (
self.old_start,
self.old_lines,
self.new_start,
self.new_lines,
) != (old_start, old_lines, new_start, new_lines)
{
return Err(CodeChangeValidationError::InconsistentHunkRange);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CodeChange {
path: String,
kind: CodeChangeKind,
hunks: Vec<CodeChangeHunk>,
truncated: bool,
truncation: Option<CodeChangeTruncation>,
patch: Option<String>,
first_changed_line: Option<u32>,
}
impl CodeChange {
#[allow(clippy::too_many_arguments)]
pub fn new(
path: impl Into<String>,
kind: CodeChangeKind,
hunks: Vec<CodeChangeHunk>,
truncated: bool,
truncation: Option<CodeChangeTruncation>,
unified_patch: Option<String>,
first_changed_line: Option<u32>,
) -> Result<Self, CodeChangeValidationError> {
let change = Self {
path: path.into(),
kind,
hunks,
truncated,
truncation,
patch: unified_patch,
first_changed_line,
};
change.validate()?;
Ok(change)
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub const fn kind(&self) -> CodeChangeKind {
self.kind
}
#[must_use]
pub fn hunks(&self) -> &[CodeChangeHunk] {
&self.hunks
}
#[must_use]
pub const fn truncated(&self) -> bool {
self.truncated
}
#[must_use]
pub const fn truncation(&self) -> Option<CodeChangeTruncation> {
self.truncation
}
#[must_use]
pub fn patch(&self) -> Option<&str> {
self.patch.as_deref()
}
#[must_use]
pub const fn first_changed_line(&self) -> Option<u32> {
self.first_changed_line
}
fn validate(&self) -> Result<(), CodeChangeValidationError> {
if self.path.is_empty()
|| self.path.len() > MAX_CODE_CHANGE_PATH_BYTES
|| self.path.contains('\0')
{
return Err(CodeChangeValidationError::InvalidPath);
}
if self.hunks.len() > MAX_CODE_CHANGE_HUNKS {
return Err(CodeChangeValidationError::TooManyHunks);
}
let mut total_lines = 0usize;
for hunk in &self.hunks {
hunk.validate()?;
total_lines = total_lines.saturating_add(hunk.lines.len());
}
if total_lines > MAX_CODE_CHANGE_LINES {
return Err(CodeChangeValidationError::TooManyLines);
}
if self
.patch
.as_ref()
.is_some_and(|patch| patch.len() > MAX_CODE_CHANGE_PATCH_BYTES)
{
return Err(CodeChangeValidationError::PatchTooLong);
}
if self.first_changed_line.is_some_and(|line| line == 0) {
return Err(CodeChangeValidationError::InvalidFirstChangedLine);
}
if self.truncated == self.truncation.is_none() {
return Err(CodeChangeValidationError::InconsistentTruncation);
}
Ok(())
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CodeChangeDef<'a> {
path: &'a str,
kind: CodeChangeKind,
hunks: &'a [CodeChangeHunk],
truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
truncation: Option<CodeChangeTruncation>,
#[serde(skip_serializing_if = "Option::is_none")]
patch: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
first_changed_line: Option<u32>,
}
impl Serialize for CodeChange {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.validate().map_err(serde::ser::Error::custom)?;
CodeChangeDef {
path: &self.path,
kind: self.kind,
hunks: &self.hunks,
truncated: self.truncated,
truncation: self.truncation,
patch: self.patch.as_deref(),
first_changed_line: self.first_changed_line,
}
.serialize(serializer)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct RawCodeChange {
path: String,
kind: CodeChangeKind,
hunks: Vec<CodeChangeHunk>,
truncated: bool,
#[serde(default)]
truncation: Option<CodeChangeTruncation>,
#[serde(default)]
patch: Option<String>,
#[serde(default)]
first_changed_line: Option<u32>,
}
impl<'de> Deserialize<'de> for CodeChange {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = RawCodeChange::deserialize(deserializer)?;
Self::new(
raw.path,
raw.kind,
raw.hunks,
raw.truncated,
raw.truncation,
raw.patch,
raw.first_changed_line,
)
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum CodeChangeValidationError {
#[error("code-change path is invalid")]
InvalidPath,
#[error("code-change has too many hunks")]
TooManyHunks,
#[error("code-change has too many lines")]
TooManyLines,
#[error("code-change hunk line count is invalid")]
InvalidHunkLines,
#[error("code-change line is too long")]
LineTooLong,
#[error("code-change line numbers are invalid")]
InvalidLineNumbers,
#[error("code-change hunk range is inconsistent")]
InconsistentHunkRange,
#[error("code-change patch is too long")]
PatchTooLong,
#[error("code-change first changed line is invalid")]
InvalidFirstChangedLine,
#[error("code-change truncation state is inconsistent")]
InconsistentTruncation,
}