use tonic::{Request, transport::Channel};
use crate::error::{ClientError, Result};
use vectordb_proto::editing_service_client::EditingServiceClient;
use vectordb_proto::editing::{
EditCodeRequest, EditCodeResponse, ValidateEditRequest, ValidateEditResponse,
EditTarget, LineRange, SemanticElement, EditOptions, ValidationIssue,
};
pub struct EditingClient {
client: EditingServiceClient<Channel>,
}
impl EditingClient {
pub fn new(channel: Channel) -> Self {
let client = EditingServiceClient::new(channel);
Self { client }
}
pub async fn edit_code(
&mut self,
file_path: String,
target: EditFileTarget,
content: String,
options: Option<EditFileOptions>,
) -> Result<EditCodeResponse> {
let proto_target = self.convert_target(target)?;
let proto_options = options.map(convert_options);
let request = Request::new(EditCodeRequest {
file_path,
target: Some(proto_target),
content,
options: proto_options,
});
let response = self.client.edit_code(request).await?;
Ok(response.into_inner())
}
pub async fn validate_edit(
&mut self,
file_path: String,
target: EditFileTarget,
content: String,
options: Option<EditFileOptions>,
) -> Result<ValidateEditResponse> {
let proto_target = self.convert_target(target)?;
let proto_options = options.map(convert_options);
let request = Request::new(ValidateEditRequest {
file_path,
target: Some(proto_target),
content,
options: proto_options,
});
let response = self.client.validate_edit(request).await?;
Ok(response.into_inner())
}
fn convert_target(&self, target: EditFileTarget) -> Result<EditTarget> {
let target_type = match target {
EditFileTarget::LineRange { start, end } => {
if start == 0 || end == 0 {
return Err(ClientError::InvalidArgument("Line numbers must be 1-based (starting from 1)".into()));
}
if start > end {
return Err(ClientError::InvalidArgument(format!(
"Start line ({}) cannot be greater than end line ({})",
start, end
)));
}
EditTarget {
target_type: Some(vectordb_proto::editing::edit_target::TargetType::LineRange(
LineRange {
start_line: start,
end_line: end,
}
)),
}
},
EditFileTarget::Semantic { element_query } => {
if element_query.is_empty() {
return Err(ClientError::InvalidArgument("Element query cannot be empty".into()));
}
EditTarget {
target_type: Some(vectordb_proto::editing::edit_target::TargetType::SemanticElement(
SemanticElement {
element_query,
}
)),
}
}
};
Ok(target_type)
}
}
#[derive(Debug, Clone)]
pub enum EditFileTarget {
LineRange {
start: u32,
end: u32,
},
Semantic {
element_query: String,
},
}
#[derive(Debug, Clone)]
pub struct EditFileOptions {
pub update_references: bool,
pub preserve_documentation: bool,
pub format_code: bool,
}
fn convert_options(options: EditFileOptions) -> EditOptions {
EditOptions {
update_references: options.update_references,
preserve_documentation: options.preserve_documentation,
format_code: options.format_code,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct ValidationIssueInfo {
pub severity: ValidationSeverity,
pub message: String,
pub line_number: Option<u32>,
}
impl From<ValidationIssue> for ValidationIssueInfo {
fn from(issue: ValidationIssue) -> Self {
let severity = match issue.severity() {
vectordb_proto::editing::validation_issue::Severity::Info => ValidationSeverity::Info,
vectordb_proto::editing::validation_issue::Severity::Warning => ValidationSeverity::Warning,
vectordb_proto::editing::validation_issue::Severity::Error => ValidationSeverity::Error,
};
Self {
severity,
message: issue.message,
line_number: issue.line_number,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_line_range_target() {
let client = MockEditingClient {};
let target = EditFileTarget::LineRange { start: 10, end: 20 };
let result = client.convert_target(target).unwrap();
match result.target_type {
Some(vectordb_proto::editing::edit_target::TargetType::LineRange(line_range)) => {
assert_eq!(line_range.start_line, 10);
assert_eq!(line_range.end_line, 20);
},
_ => panic!("Expected LineRange target type"),
}
}
#[test]
fn test_convert_semantic_target() {
let client = MockEditingClient {};
let target = EditFileTarget::Semantic { element_query: "function:process_data".to_string() };
let result = client.convert_target(target).unwrap();
match result.target_type {
Some(vectordb_proto::editing::edit_target::TargetType::SemanticElement(element)) => {
assert_eq!(element.element_query, "function:process_data");
},
_ => panic!("Expected SemanticElement target type"),
}
}
#[test]
fn test_invalid_line_range() {
let client = MockEditingClient {};
let target = EditFileTarget::LineRange { start: 0, end: 20 };
let result = client.convert_target(target);
assert!(result.is_err());
let target = EditFileTarget::LineRange { start: 30, end: 20 };
let result = client.convert_target(target);
assert!(result.is_err());
}
#[test]
fn test_empty_element_query() {
let client = MockEditingClient {};
let target = EditFileTarget::Semantic { element_query: "".to_string() };
let result = client.convert_target(target);
assert!(result.is_err());
}
struct MockEditingClient {}
impl MockEditingClient {
fn convert_target(&self, target: EditFileTarget) -> Result<EditTarget> {
match target {
EditFileTarget::LineRange { start, end } => {
if start == 0 || end == 0 {
return Err(ClientError::InvalidArgument("Line numbers must be 1-based (starting from 1)".into()));
}
if start > end {
return Err(ClientError::InvalidArgument(format!(
"Start line ({}) cannot be greater than end line ({})",
start, end
)));
}
Ok(EditTarget {
target_type: Some(vectordb_proto::editing::edit_target::TargetType::LineRange(
LineRange {
start_line: start,
end_line: end,
}
)),
})
},
EditFileTarget::Semantic { element_query } => {
if element_query.is_empty() {
return Err(ClientError::InvalidArgument("Element query cannot be empty".into()));
}
Ok(EditTarget {
target_type: Some(vectordb_proto::editing::edit_target::TargetType::SemanticElement(
SemanticElement {
element_query,
}
)),
})
}
}
}
}
}