crw_server/routes/
change_tracking.rs1use axum::Json;
22use axum::extract::State;
23use axum::extract::rejection::JsonRejection;
24use crw_core::error::CrwError;
25use crw_core::types::{
26 ApiResponse, ChangeTrackingMode, ChangeTrackingOptions, ChangeTrackingResult,
27 ChangeTrackingSnapshot,
28};
29use serde::Deserialize;
30use serde_json::Value;
31
32use crate::error::AppError;
33use crate::state::AppState;
34
35#[derive(Debug, Clone, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct DiffCurrent {
39 #[serde(default)]
40 pub markdown: Option<String>,
41 #[serde(default)]
42 pub json: Option<Value>,
43}
44
45#[derive(Debug, Clone, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct DiffItem {
49 #[serde(default)]
50 pub url: Option<String>,
51 #[serde(default)]
52 pub current: Option<DiffCurrent>,
53 #[serde(default)]
54 pub previous: Option<ChangeTrackingSnapshot>,
55 #[serde(default)]
56 pub modes: Option<Vec<ChangeTrackingMode>>,
57 #[serde(default)]
58 pub schema: Option<Value>,
59 #[serde(default)]
60 pub prompt: Option<String>,
61 #[serde(default, alias = "content_type")]
62 pub content_type: Option<String>,
63 #[serde(default)]
64 pub tag: Option<String>,
65 #[serde(default)]
68 pub goal: Option<String>,
69 #[serde(default, alias = "judge_enabled")]
70 pub judge_enabled: Option<bool>,
71}
72
73#[derive(Debug, Clone, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct DiffRequest {
79 #[serde(default)]
80 pub batch: Option<Vec<DiffItem>>,
81 #[serde(default)]
83 pub current: Option<DiffCurrent>,
84 #[serde(default)]
85 pub previous: Option<ChangeTrackingSnapshot>,
86 #[serde(default)]
87 pub modes: Option<Vec<ChangeTrackingMode>>,
88 #[serde(default)]
89 pub schema: Option<Value>,
90 #[serde(default)]
91 pub prompt: Option<String>,
92 #[serde(default, alias = "content_type")]
93 pub content_type: Option<String>,
94 #[serde(default)]
95 pub tag: Option<String>,
96 #[serde(default)]
97 pub goal: Option<String>,
98 #[serde(default, alias = "judge_enabled")]
99 pub judge_enabled: Option<bool>,
100}
101
102fn default_modes() -> Vec<ChangeTrackingMode> {
103 vec![ChangeTrackingMode::GitDiff]
104}
105
106fn diff_one(
108 item: &DiffItem,
109 shared_modes: &Option<Vec<ChangeTrackingMode>>,
110 shared_schema: &Option<Value>,
111 shared_prompt: &Option<String>,
112 shared_content_type: &Option<String>,
113) -> Result<ChangeTrackingResult, CrwError> {
114 let current = item.current.as_ref().ok_or_else(|| {
115 CrwError::InvalidRequest("each diff item requires a 'current' object".into())
116 })?;
117
118 let modes = item
119 .modes
120 .clone()
121 .or_else(|| shared_modes.clone())
122 .unwrap_or_else(default_modes);
123
124 let opts = ChangeTrackingOptions {
125 modes,
126 schema: item.schema.clone().or_else(|| shared_schema.clone()),
127 prompt: item.prompt.clone().or_else(|| shared_prompt.clone()),
128 previous: item.previous.clone(),
129 tag: item.tag.clone(),
130 content_type: item
131 .content_type
132 .clone()
133 .or_else(|| shared_content_type.clone()),
134 };
135
136 let markdown = current.markdown.as_deref().unwrap_or("");
137 Ok(crw_diff::compute_change_tracking(
138 &opts,
139 markdown,
140 current.json.as_ref(),
141 opts.content_type.as_deref(),
142 ))
143}
144
145pub async fn diff(
146 State(_state): State<AppState>,
147 body: Result<Json<DiffRequest>, JsonRejection>,
148) -> Result<Json<ApiResponse<Value>>, AppError> {
149 let Json(req) = body.map_err(AppError::from)?;
150
151 if let Some(items) = &req.batch {
153 if items.is_empty() {
154 return Err(AppError::from(CrwError::InvalidRequest(
155 "'batch' must contain at least one item".into(),
156 )));
157 }
158 let mut results: Vec<ChangeTrackingResult> = Vec::with_capacity(items.len());
159 for item in items {
160 results.push(diff_one(
161 item,
162 &req.modes,
163 &req.schema,
164 &req.prompt,
165 &req.content_type,
166 )?);
167 }
168 let data = serde_json::to_value(results)
169 .map_err(|e| CrwError::Internal(format!("failed to serialize diff results: {e}")))?;
170 return Ok(Json(ApiResponse::ok(data)));
171 }
172
173 let single = DiffItem {
175 url: None,
176 current: req.current.clone(),
177 previous: req.previous.clone(),
178 modes: req.modes.clone(),
179 schema: req.schema.clone(),
180 prompt: req.prompt.clone(),
181 content_type: req.content_type.clone(),
182 tag: req.tag.clone(),
183 goal: req.goal.clone(),
184 judge_enabled: req.judge_enabled,
185 };
186 let result = diff_one(&single, &None, &None, &None, &None)?;
187 let data = serde_json::to_value(result)
188 .map_err(|e| CrwError::Internal(format!("failed to serialize diff result: {e}")))?;
189 Ok(Json(ApiResponse::ok(data)))
190}