1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use thiserror::Error;
3
4use crate::WebFetchPresentation;
5
6pub const MAX_CODE_CHANGE_PATH_BYTES: usize = 4_096;
8pub const MAX_CODE_CHANGE_HUNKS: usize = 32;
10pub const MAX_CODE_CHANGE_LINES_PER_HUNK: usize = 128;
12pub const MAX_CODE_CHANGE_LINES: usize = 1_024;
14pub const MAX_CODE_CHANGE_LINE_BYTES: usize = 1_024;
16pub const MAX_CODE_CHANGE_PATCH_BYTES: usize = 64 * 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
25#[serde(tag = "type", content = "value", rename_all = "snake_case")]
26pub enum ToolPresentation {
27 CodeChange(CodeChange),
29 WebFetch(Box<WebFetchPresentation>),
31}
32
33impl ToolPresentation {
34 #[must_use]
36 pub const fn code_change(&self) -> Option<&CodeChange> {
37 match self {
38 Self::CodeChange(change) => Some(change),
39 Self::WebFetch(_) => None,
40 }
41 }
42
43 #[must_use]
45 pub fn web_fetch(&self) -> Option<&WebFetchPresentation> {
46 match self {
47 Self::CodeChange(_) => None,
48 Self::WebFetch(fetch) => Some(fetch.as_ref()),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum CodeChangeKind {
57 Create,
59 Update,
61 Delete,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum CodeChangeTruncation {
69 Hunks,
71 Lines,
73 LineBytes,
75 PatchBytes,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum CodeChangeLineKind {
83 Context,
85 Addition,
87 Deletion,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94pub struct CodeChangeLine {
95 kind: CodeChangeLineKind,
96 old_line: Option<u32>,
97 new_line: Option<u32>,
98 text: String,
99}
100
101impl CodeChangeLine {
102 pub fn new(
109 kind: CodeChangeLineKind,
110 old_line: Option<u32>,
111 new_line: Option<u32>,
112 text: impl Into<String>,
113 ) -> Result<Self, CodeChangeValidationError> {
114 let line = Self {
115 kind,
116 old_line,
117 new_line,
118 text: text.into(),
119 };
120 line.validate()?;
121 Ok(line)
122 }
123
124 #[must_use]
126 pub const fn kind(&self) -> CodeChangeLineKind {
127 self.kind
128 }
129
130 #[must_use]
132 pub const fn old_line(&self) -> Option<u32> {
133 self.old_line
134 }
135
136 #[must_use]
138 pub const fn new_line(&self) -> Option<u32> {
139 self.new_line
140 }
141
142 #[must_use]
144 pub fn text(&self) -> &str {
145 &self.text
146 }
147
148 fn validate(&self) -> Result<(), CodeChangeValidationError> {
149 if self.text.len() > MAX_CODE_CHANGE_LINE_BYTES {
150 return Err(CodeChangeValidationError::LineTooLong);
151 }
152 let positive = |line: Option<u32>| line.is_some_and(|line| line > 0);
153 let valid_numbers = match self.kind {
154 CodeChangeLineKind::Context => positive(self.old_line) && positive(self.new_line),
155 CodeChangeLineKind::Addition => self.old_line.is_none() && positive(self.new_line),
156 CodeChangeLineKind::Deletion => positive(self.old_line) && self.new_line.is_none(),
157 };
158 if valid_numbers {
159 Ok(())
160 } else {
161 Err(CodeChangeValidationError::InvalidLineNumbers)
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct CodeChangeHunk {
170 old_start: u32,
171 old_lines: u32,
172 new_start: u32,
173 new_lines: u32,
174 lines: Vec<CodeChangeLine>,
175}
176
177impl CodeChangeHunk {
178 pub fn new(
185 old_start: u32,
186 old_lines: u32,
187 new_start: u32,
188 new_lines: u32,
189 lines: Vec<CodeChangeLine>,
190 ) -> Result<Self, CodeChangeValidationError> {
191 let hunk = Self {
192 old_start,
193 old_lines,
194 new_start,
195 new_lines,
196 lines,
197 };
198 hunk.validate()?;
199 Ok(hunk)
200 }
201
202 #[must_use]
204 pub const fn old_start(&self) -> u32 {
205 self.old_start
206 }
207
208 #[must_use]
210 pub const fn old_lines(&self) -> u32 {
211 self.old_lines
212 }
213
214 #[must_use]
216 pub const fn new_start(&self) -> u32 {
217 self.new_start
218 }
219
220 #[must_use]
222 pub const fn new_lines(&self) -> u32 {
223 self.new_lines
224 }
225
226 #[must_use]
228 pub fn lines(&self) -> &[CodeChangeLine] {
229 &self.lines
230 }
231
232 fn validate(&self) -> Result<(), CodeChangeValidationError> {
233 if self.lines.is_empty() || self.lines.len() > MAX_CODE_CHANGE_LINES_PER_HUNK {
234 return Err(CodeChangeValidationError::InvalidHunkLines);
235 }
236 for line in &self.lines {
237 line.validate()?;
238 }
239 let old_start = self
240 .lines
241 .iter()
242 .find_map(CodeChangeLine::old_line)
243 .unwrap_or(0);
244 let new_start = self
245 .lines
246 .iter()
247 .find_map(CodeChangeLine::new_line)
248 .unwrap_or(0);
249 let old_lines = u32::try_from(
250 self.lines
251 .iter()
252 .filter(|line| line.old_line().is_some())
253 .count(),
254 )
255 .unwrap_or(u32::MAX);
256 let new_lines = u32::try_from(
257 self.lines
258 .iter()
259 .filter(|line| line.new_line().is_some())
260 .count(),
261 )
262 .unwrap_or(u32::MAX);
263 if (
264 self.old_start,
265 self.old_lines,
266 self.new_start,
267 self.new_lines,
268 ) != (old_start, old_lines, new_start, new_lines)
269 {
270 return Err(CodeChangeValidationError::InconsistentHunkRange);
271 }
272 Ok(())
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
278pub struct CodeChange {
279 path: String,
280 kind: CodeChangeKind,
281 hunks: Vec<CodeChangeHunk>,
282 truncated: bool,
283 truncation: Option<CodeChangeTruncation>,
284 patch: Option<String>,
285 first_changed_line: Option<u32>,
286}
287
288impl CodeChange {
289 #[allow(clippy::too_many_arguments)]
296 pub fn new(
297 path: impl Into<String>,
298 kind: CodeChangeKind,
299 hunks: Vec<CodeChangeHunk>,
300 truncated: bool,
301 truncation: Option<CodeChangeTruncation>,
302 unified_patch: Option<String>,
303 first_changed_line: Option<u32>,
304 ) -> Result<Self, CodeChangeValidationError> {
305 let change = Self {
306 path: path.into(),
307 kind,
308 hunks,
309 truncated,
310 truncation,
311 patch: unified_patch,
312 first_changed_line,
313 };
314 change.validate()?;
315 Ok(change)
316 }
317
318 #[must_use]
320 pub fn path(&self) -> &str {
321 &self.path
322 }
323
324 #[must_use]
326 pub const fn kind(&self) -> CodeChangeKind {
327 self.kind
328 }
329
330 #[must_use]
332 pub fn hunks(&self) -> &[CodeChangeHunk] {
333 &self.hunks
334 }
335
336 #[must_use]
338 pub const fn truncated(&self) -> bool {
339 self.truncated
340 }
341
342 #[must_use]
344 pub const fn truncation(&self) -> Option<CodeChangeTruncation> {
345 self.truncation
346 }
347
348 #[must_use]
350 pub fn patch(&self) -> Option<&str> {
351 self.patch.as_deref()
352 }
353
354 #[must_use]
356 pub const fn first_changed_line(&self) -> Option<u32> {
357 self.first_changed_line
358 }
359
360 fn validate(&self) -> Result<(), CodeChangeValidationError> {
361 if self.path.is_empty()
362 || self.path.len() > MAX_CODE_CHANGE_PATH_BYTES
363 || self.path.contains('\0')
364 {
365 return Err(CodeChangeValidationError::InvalidPath);
366 }
367 if self.hunks.len() > MAX_CODE_CHANGE_HUNKS {
368 return Err(CodeChangeValidationError::TooManyHunks);
369 }
370 let mut total_lines = 0usize;
371 for hunk in &self.hunks {
372 hunk.validate()?;
373 total_lines = total_lines.saturating_add(hunk.lines.len());
374 }
375 if total_lines > MAX_CODE_CHANGE_LINES {
376 return Err(CodeChangeValidationError::TooManyLines);
377 }
378 if self
379 .patch
380 .as_ref()
381 .is_some_and(|patch| patch.len() > MAX_CODE_CHANGE_PATCH_BYTES)
382 {
383 return Err(CodeChangeValidationError::PatchTooLong);
384 }
385 if self.first_changed_line.is_some_and(|line| line == 0) {
386 return Err(CodeChangeValidationError::InvalidFirstChangedLine);
387 }
388 if self.truncated == self.truncation.is_none() {
389 return Err(CodeChangeValidationError::InconsistentTruncation);
390 }
391 Ok(())
392 }
393}
394
395#[derive(Serialize)]
396#[serde(rename_all = "camelCase")]
397struct CodeChangeDef<'a> {
398 path: &'a str,
399 kind: CodeChangeKind,
400 hunks: &'a [CodeChangeHunk],
401 truncated: bool,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 truncation: Option<CodeChangeTruncation>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 patch: Option<&'a str>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 first_changed_line: Option<u32>,
408}
409
410impl Serialize for CodeChange {
411 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
412 where
413 S: Serializer,
414 {
415 self.validate().map_err(serde::ser::Error::custom)?;
416 CodeChangeDef {
417 path: &self.path,
418 kind: self.kind,
419 hunks: &self.hunks,
420 truncated: self.truncated,
421 truncation: self.truncation,
422 patch: self.patch.as_deref(),
423 first_changed_line: self.first_changed_line,
424 }
425 .serialize(serializer)
426 }
427}
428
429#[derive(Deserialize)]
430#[serde(rename_all = "camelCase", deny_unknown_fields)]
431struct RawCodeChange {
432 path: String,
433 kind: CodeChangeKind,
434 hunks: Vec<CodeChangeHunk>,
435 truncated: bool,
436 #[serde(default)]
437 truncation: Option<CodeChangeTruncation>,
438 #[serde(default)]
439 patch: Option<String>,
440 #[serde(default)]
441 first_changed_line: Option<u32>,
442}
443
444impl<'de> Deserialize<'de> for CodeChange {
445 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
446 where
447 D: Deserializer<'de>,
448 {
449 let raw = RawCodeChange::deserialize(deserializer)?;
450 Self::new(
451 raw.path,
452 raw.kind,
453 raw.hunks,
454 raw.truncated,
455 raw.truncation,
456 raw.patch,
457 raw.first_changed_line,
458 )
459 .map_err(serde::de::Error::custom)
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
465pub enum CodeChangeValidationError {
466 #[error("code-change path is invalid")]
468 InvalidPath,
469 #[error("code-change has too many hunks")]
471 TooManyHunks,
472 #[error("code-change has too many lines")]
474 TooManyLines,
475 #[error("code-change hunk line count is invalid")]
477 InvalidHunkLines,
478 #[error("code-change line is too long")]
480 LineTooLong,
481 #[error("code-change line numbers are invalid")]
483 InvalidLineNumbers,
484 #[error("code-change hunk range is inconsistent")]
486 InconsistentHunkRange,
487 #[error("code-change patch is too long")]
489 PatchTooLong,
490 #[error("code-change first changed line is invalid")]
492 InvalidFirstChangedLine,
493 #[error("code-change truncation state is inconsistent")]
495 InconsistentTruncation,
496}