use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: &str = "1.0.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageType {
Result,
Error,
Begin,
End,
Summary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Success,
NotFound,
PartialSuccess,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ResultCode {
Ok,
NotFound,
ParseError,
IndexError,
InvalidQuery,
InternalError,
}
impl ResultCode {
pub fn as_str(&self) -> &'static str {
match self {
Self::Ok => "OK",
Self::NotFound => "NOT_FOUND",
Self::ParseError => "PARSE_ERROR",
Self::IndexError => "INDEX_ERROR",
Self::InvalidQuery => "INVALID_QUERY",
Self::InternalError => "INTERNAL_ERROR",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityType {
Symbol,
SearchResult,
CallTree,
ImpactGraph,
Document,
Callers,
Calls,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope<T = serde_json::Value> {
#[serde(rename = "type")]
pub message_type: MessageType,
pub status: Status,
pub code: ResultCode,
pub exit_code: u8,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorDetails>,
pub meta: Meta,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetails {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub suggestions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Meta {
pub schema_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub entity_type: Option<EntityType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub depth: Option<u32>,
}
impl Default for Meta {
fn default() -> Self {
Self {
schema_version: SCHEMA_VERSION.to_string(),
entity_type: None,
count: None,
query: None,
lang: None,
duration_ms: None,
truncated: None,
depth: None,
}
}
}
impl<T> Envelope<T> {
pub fn success(data: T) -> Self {
Self {
message_type: MessageType::Result,
status: Status::Success,
code: ResultCode::Ok,
exit_code: 0,
message: "Operation completed successfully".to_string(),
hint: None,
data: Some(data),
error: None,
meta: Meta::default(),
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self {
message_type: MessageType::Result,
status: Status::NotFound,
code: ResultCode::NotFound,
exit_code: 1,
message: message.into(),
hint: None,
data: None,
error: None,
meta: Meta::default(),
}
}
pub fn error(code: ResultCode, message: impl Into<String>) -> Self {
Self {
message_type: MessageType::Error,
status: Status::Error,
code,
exit_code: 2,
message: message.into(),
hint: None,
data: None,
error: None,
meta: Meta::default(),
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_entity_type(mut self, entity_type: EntityType) -> Self {
self.meta.entity_type = Some(entity_type);
self
}
pub fn with_count(mut self, count: usize) -> Self {
self.meta.count = Some(count);
self
}
pub fn with_query(mut self, query: impl Into<String>) -> Self {
self.meta.query = Some(query.into());
self
}
pub fn with_lang(mut self, lang: impl Into<String>) -> Self {
self.meta.lang = Some(lang.into());
self
}
pub fn with_duration_ms(mut self, duration_ms: u64) -> Self {
self.meta.duration_ms = Some(duration_ms);
self
}
pub fn with_error_details(mut self, details: ErrorDetails) -> Self {
self.error = Some(details);
self
}
pub fn with_truncated(mut self, truncated: bool) -> Self {
self.meta.truncated = Some(truncated);
self
}
pub fn with_depth(mut self, depth: u32) -> Self {
self.meta.depth = Some(depth);
self
}
pub fn to_json(&self) -> Result<String, serde_json::Error>
where
T: Serialize,
{
serde_json::to_string_pretty(self)
}
pub fn to_json_compact(&self) -> Result<String, serde_json::Error>
where
T: Serialize,
{
serde_json::to_string(self)
}
pub fn to_json_with_fields(&self, fields: &[String]) -> Result<String, serde_json::Error>
where
T: Serialize,
{
let mut value = serde_json::to_value(self)?;
fn filter_object(obj: &mut serde_json::Map<String, serde_json::Value>, fields: &[String]) {
let keys_to_remove: Vec<String> = obj
.keys()
.filter(|k| !fields.contains(k))
.cloned()
.collect();
for key in keys_to_remove {
obj.remove(&key);
}
}
if let Some(data) = value.get_mut("data") {
if let Some(arr) = data.as_array_mut() {
for item in arr.iter_mut() {
if let Some(obj) = item.as_object_mut() {
filter_object(obj, fields);
}
}
} else if let Some(obj) = data.as_object_mut() {
filter_object(obj, fields);
}
}
serde_json::to_string_pretty(&value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success_envelope() {
let data = vec!["item1", "item2"];
let envelope = Envelope::success(data)
.with_entity_type(EntityType::Symbol)
.with_count(2)
.with_message("Found 2 symbols");
assert_eq!(envelope.message_type, MessageType::Result);
assert_eq!(envelope.status, Status::Success);
assert_eq!(envelope.code, ResultCode::Ok);
assert_eq!(envelope.exit_code, 0);
assert_eq!(envelope.meta.count, Some(2));
assert!(envelope.data.is_some());
}
#[test]
fn test_not_found_envelope() {
let envelope: Envelope<()> = Envelope::not_found("Symbol 'foo' not found")
.with_hint("Try semantic_search_with_context");
assert_eq!(envelope.status, Status::NotFound);
assert_eq!(envelope.code, ResultCode::NotFound);
assert_eq!(envelope.exit_code, 1);
assert!(envelope.data.is_none());
assert!(envelope.hint.is_some());
}
#[test]
fn test_error_envelope() {
let envelope: Envelope<()> = Envelope::error(ResultCode::ParseError, "Invalid syntax")
.with_error_details(ErrorDetails {
suggestions: vec!["Check syntax".to_string()],
context: None,
});
assert_eq!(envelope.message_type, MessageType::Error);
assert_eq!(envelope.status, Status::Error);
assert_eq!(envelope.code, ResultCode::ParseError);
assert_eq!(envelope.exit_code, 2);
assert!(envelope.error.is_some());
}
#[test]
fn test_json_serialization() {
let envelope = Envelope::success(vec!["a", "b"])
.with_entity_type(EntityType::Symbol)
.with_count(2);
let json = envelope.to_json().unwrap();
assert!(json.contains("\"type\": \"result\""));
assert!(json.contains("\"status\": \"success\""));
assert!(json.contains("\"schema_version\": \"1.0.0\""));
}
}