use serde::{Deserialize, Serialize};
use crate::relationships::Relationships;
pub const OPERATION_SCHEMA_VERSION: &str = "2.0.0";
pub const QUERY_SCHEMA_VERSION: &str = "1.0.0";
pub const TOOL_NAME: &str = "splice";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonResponse<T> {
pub schema_version: String,
pub execution_id: String,
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial: Option<bool>,
}
impl<T> JsonResponse<T> {
pub fn new(data: T, execution_id: &str) -> Self {
JsonResponse {
schema_version: QUERY_SCHEMA_VERSION.to_string(),
execution_id: execution_id.to_string(),
data,
tool: Some(TOOL_NAME.to_string()),
timestamp: Some(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
partial: None,
}
}
pub fn with_partial(mut self, partial: bool) -> Self {
self.partial = Some(partial);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationResult {
pub schema_version: String,
pub execution_id: String,
pub operation_type: String,
pub status: String,
pub message: String,
pub tool: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<OperationData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<super::span_result::ErrorDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_report: Option<serde_json::Value>,
}
impl OperationResult {
pub fn new(operation_type: String) -> Self {
Self::with_execution_id(operation_type, None)
}
pub fn with_execution_id(operation_type: String, execution_id: Option<String>) -> Self {
use uuid::Uuid;
Self {
schema_version: OPERATION_SCHEMA_VERSION.to_string(),
execution_id: execution_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
operation_type,
status: "ok".to_string(),
message: String::new(),
tool: TOOL_NAME.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
workspace: None,
result: None,
error: None,
preview_report: None,
}
}
pub fn set_execution_id(mut self, execution_id: String) -> Self {
self.execution_id = execution_id;
self
}
pub fn success(mut self, message: String) -> Self {
self.status = "ok".to_string();
self.message = message;
self
}
pub fn error(mut self, message: String, error: super::span_result::ErrorDetails) -> Self {
self.status = "error".to_string();
self.message = message;
self.error = Some(error);
self
}
pub fn with_workspace(mut self, workspace: String) -> Self {
self.workspace = Some(workspace);
self
}
pub fn with_result(mut self, result: OperationData) -> Self {
self.result = Some(result);
self
}
pub fn with_preview_report(mut self, preview_report: serde_json::Value) -> Self {
self.preview_report = Some(preview_report);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OperationData {
#[serde(rename = "patch")]
Patch(PatchResult),
#[serde(rename = "delete")]
Delete(DeleteResult),
#[serde(rename = "plan")]
Plan(PlanResult),
#[serde(rename = "query")]
Query(QueryResult),
#[serde(rename = "apply_files")]
ApplyFiles(ApplyFilesResult),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchResult {
pub file: String,
pub symbol: String,
pub kind: String,
pub spans: Vec<super::span_result::SpanResult>,
pub before_hash: String,
pub after_hash: String,
pub lines_added: usize,
pub lines_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResult {
pub file: String,
pub symbol: String,
pub kind: String,
pub spans: Vec<super::span_result::SpanResult>,
pub bytes_removed: usize,
pub lines_removed: usize,
pub references_removed: usize,
pub file_checksum_before: String,
pub span_checksums: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanResult {
pub total_steps: usize,
pub steps_completed: usize,
pub steps: Vec<StepResult>,
pub files_affected: Vec<String>,
pub total_bytes_changed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct StepResult {
pub step: usize,
pub status: String,
pub message: String,
pub file: String,
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub labels: Vec<String>,
pub count: usize,
pub symbols: Vec<super::span_result::SpanResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_symbols: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_reasons: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabelQueryResponse {
pub labels: Vec<String>,
pub symbols: Vec<SymbolMatch>,
pub count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_symbols: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_reasons: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetResponse {
pub symbol: SymbolMatch,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyFilesResult {
pub glob_pattern: String,
pub find_pattern: String,
pub replace_pattern: String,
pub files_matched: usize,
pub files_modified: usize,
pub files: Vec<FilePatternResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilePatternResult {
pub file: String,
pub matches: usize,
pub replacements: usize,
pub spans: Vec<super::span_result::SpanResult>,
pub before_hash: String,
pub after_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanContext {
pub before: Vec<String>,
pub selected: Vec<String>,
pub after: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanSemantics {
pub kind: String,
pub language: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SpanChecksums {
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_checksum_before: Option<String>,
}
pub fn generate_span_id(file_path: &str, byte_start: usize, byte_end: usize) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(byte_start.to_be_bytes());
hasher.update(b":");
hasher.update(byte_end.to_be_bytes());
let result = hasher.finalize();
format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
)
}
pub fn normalize_checksum(value: String) -> String {
if value.starts_with("sha256:") {
value
} else {
format!("sha256:{}", value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Span {
pub span_id: String,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<SpanContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantics: Option<SpanSemantics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationships: Option<Relationships>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksums: Option<SpanChecksums>,
}
impl Span {
pub fn new(
file_path: String,
byte_start: usize,
byte_end: usize,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
) -> Self {
let span_id = generate_span_id(&file_path, byte_start, byte_end);
Span {
span_id,
file_path,
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
context: None,
semantics: None,
relationships: None,
checksums: None,
}
}
pub fn generate_id(file_path: &str, byte_start: usize, byte_end: usize) -> String {
generate_span_id(file_path, byte_start, byte_end)
}
pub fn with_context(mut self, context: SpanContext) -> Self {
self.context = Some(context);
self
}
pub fn with_semantics(mut self, semantics: SpanSemantics) -> Self {
self.semantics = Some(semantics);
self
}
pub fn with_relationships(mut self, relationships: Relationships) -> Self {
self.relationships = Some(relationships);
self
}
pub fn with_checksums(mut self, checksums: SpanChecksums) -> Self {
self.checksums = Some(checksums);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolMatch {
pub match_id: String,
pub span: Span,
pub name: String,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_id: Option<String>,
}
impl SymbolMatch {
pub fn generate_match_id(symbol_name: &str, file_path: &str, byte_start: usize) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
symbol_name.hash(&mut hasher);
file_path.hash(&mut hasher);
byte_start.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub fn new(
name: String,
kind: String,
span: Span,
parent: Option<String>,
symbol_id: Option<String>,
) -> Self {
let match_id = Self::generate_match_id(&name, &span.file_path, span.byte_start);
SymbolMatch {
match_id,
span,
name,
kind,
parent,
symbol_id,
}
}
}