use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum FinalPayload {
Grep(GrepPayload),
Ast(AstPayload),
Semantic(SemanticPayload),
Malformed {
raw: String,
error: String,
},
}
impl FinalPayload {
pub fn parse(json_str: &str) -> Self {
let trimmed = json_str.trim();
let parsed: Result<serde_json::Value, _> = serde_json::from_str(trimmed);
match parsed {
Ok(value) => {
match serde_json::from_value::<FinalPayload>(value.clone()) {
Ok(payload) => payload,
Err(e) => {
if let Some(kind) = value.get("kind").and_then(|k| k.as_str()) {
match kind {
"grep" => serde_json::from_value(value).unwrap_or_else(|e2| {
FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("GrepPayload parse error: {}", e2),
}
}),
"ast" => serde_json::from_value(value).unwrap_or_else(|e2| {
FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("AstPayload parse error: {}", e2),
}
}),
"semantic" => serde_json::from_value(value).unwrap_or_else(|e2| {
FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("SemanticPayload parse error: {}", e2),
}
}),
_ => FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("Unknown kind: {}", kind),
},
}
} else {
FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("Missing 'kind' field: {}", e),
}
}
}
}
}
Err(e) => {
FinalPayload::Malformed {
raw: trimmed.to_string(),
error: format!("JSON parse error: {}", e),
}
}
}
}
pub fn is_verifiable(&self) -> bool {
matches!(self, FinalPayload::Grep(_) | FinalPayload::Ast(_))
}
pub fn file(&self) -> Option<&str> {
match self {
FinalPayload::Grep(p) => Some(&p.file),
FinalPayload::Ast(p) => Some(&p.file),
FinalPayload::Semantic(p) => Some(&p.file),
FinalPayload::Malformed { .. } => None,
}
}
pub fn summary(&self) -> String {
match self {
FinalPayload::Grep(p) => {
format!(
"Grep(file={}, pattern={}, {} matches)",
p.file,
p.pattern,
p.matches.len()
)
}
FinalPayload::Ast(p) => {
format!(
"Ast(file={}, query={}, {} results)",
p.file,
p.query,
p.results.len()
)
}
FinalPayload::Semantic(p) => {
let preview = if p.answer.len() > 50 {
format!("{}...", &p.answer[..50])
} else {
p.answer.clone()
};
format!("Semantic(file={}, answer={})", p.file, preview)
}
FinalPayload::Malformed { error, .. } => {
format!("Malformed({})", error)
}
}
}
}
impl fmt::Display for FinalPayload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GrepPayload {
pub file: String,
pub pattern: String,
pub matches: Vec<GrepMatch>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct GrepMatch {
pub line: usize,
pub text: String,
}
impl GrepMatch {
pub fn new(line: usize, text: String) -> Self {
Self { line, text }
}
pub fn text_matches(&self, actual_line: &str) -> bool {
actual_line.contains(&self.text)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AstPayload {
pub file: String,
pub query: String,
pub results: Vec<AstResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AstResult {
pub name: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub return_type: Option<String>,
#[serde(default)]
pub span: Option<(usize, usize)>,
}
impl AstResult {
pub fn function(
name: String,
args: Vec<String>,
return_type: Option<String>,
span: Option<(usize, usize)>,
) -> Self {
Self {
name,
args,
return_type,
span,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SemanticPayload {
pub file: String,
pub answer: String,
}
impl SemanticPayload {
pub fn new(file: String, answer: String) -> Self {
Self { file, answer }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_grep_payload() {
let json = r#"{
"kind": "grep",
"file": "src/main.rs",
"pattern": "async fn",
"matches": [
{"line": 42, "text": "async fn process() {"},
{"line": 100, "text": "async fn handle() {"}
]
}"#;
let payload = FinalPayload::parse(json);
match payload {
FinalPayload::Grep(p) => {
assert_eq!(p.file, "src/main.rs");
assert_eq!(p.pattern, "async fn");
assert_eq!(p.matches.len(), 2);
assert_eq!(p.matches[0].line, 42);
}
_ => panic!("Expected Grep payload"),
}
}
#[test]
fn parse_ast_payload() {
let json = r#"{
"kind": "ast",
"file": "src/main.rs",
"query": "functions",
"results": [
{"name": "process", "args": ["input: &str"], "return_type": "Result<String>"}
]
}"#;
let payload = FinalPayload::parse(json);
match payload {
FinalPayload::Ast(p) => {
assert_eq!(p.file, "src/main.rs");
assert_eq!(p.query, "functions");
assert_eq!(p.results.len(), 1);
assert_eq!(p.results[0].name, "process");
}
_ => panic!("Expected Ast payload"),
}
}
#[test]
fn parse_semantic_payload() {
let json = r#"{
"kind": "semantic",
"file": "src/main.rs",
"answer": "This module provides async processing."
}"#;
let payload = FinalPayload::parse(json);
match payload {
FinalPayload::Semantic(p) => {
assert_eq!(p.file, "src/main.rs");
assert!(p.answer.contains("async processing"));
}
_ => panic!("Expected Semantic payload"),
}
}
#[test]
fn parse_malformed_json() {
let json = "not valid json at all";
let payload = FinalPayload::parse(json);
match payload {
FinalPayload::Malformed { raw, error } => {
assert_eq!(raw, "not valid json at all");
assert!(error.contains("JSON parse error"));
}
_ => panic!("Expected Malformed payload"),
}
}
#[test]
fn parse_missing_kind_field() {
let json = r#"{"file": "src/main.rs", "data": "value"}"#;
let payload = FinalPayload::parse(json);
match payload {
FinalPayload::Malformed { error, .. } => {
assert!(error.contains("kind"));
}
_ => panic!("Expected Malformed payload"),
}
}
#[test]
fn malformed_payload_is_serializable() {
let payload = FinalPayload::Malformed {
raw: "oops".to_string(),
error: "parse error".to_string(),
};
let json = serde_json::to_string(&payload).expect("malformed payload should serialize");
assert!(json.contains("\"kind\":\"malformed\""));
assert!(json.contains("\"raw\":\"oops\""));
assert!(json.contains("\"error\":\"parse error\""));
}
#[test]
fn grep_match_text_matching() {
let m = GrepMatch::new(42, "async fn".to_string());
assert!(m.text_matches("pub async fn process() -> Result<()> {"));
assert!(!m.text_matches("fn process() -> Result<()> {"));
}
#[test]
fn is_verifiable() {
let grep_json = r#"{"kind": "grep", "file": "x.rs", "pattern": "fn", "matches": []}"#;
let semantic_json = r#"{"kind": "semantic", "file": "x.rs", "answer": "text"}"#;
assert!(FinalPayload::parse(grep_json).is_verifiable());
assert!(!FinalPayload::parse(semantic_json).is_verifiable());
}
#[test]
fn file_extraction() {
let grep_json =
r#"{"kind": "grep", "file": "src/main.rs", "pattern": "fn", "matches": []}"#;
assert_eq!(FinalPayload::parse(grep_json).file(), Some("src/main.rs"));
}
}