aster/map/server/
routes.rs1use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::map::server::services::{
10 architecture::{build_architecture_map, get_module_detail, get_symbol_refs},
11 dependency::{build_dependency_tree, detect_entry_points},
12};
13use crate::map::server::types::*;
14use crate::map::types_enhanced::EnhancedCodeBlueprint;
15
16#[derive(Debug, Clone)]
18pub struct ApiError {
19 pub message: String,
20 pub status_code: u16,
21}
22
23impl ApiError {
24 pub fn not_found(msg: &str) -> Self {
25 Self {
26 message: msg.to_string(),
27 status_code: 404,
28 }
29 }
30
31 pub fn bad_request(msg: &str) -> Self {
32 Self {
33 message: msg.to_string(),
34 status_code: 400,
35 }
36 }
37
38 pub fn internal(msg: &str) -> Self {
39 Self {
40 message: msg.to_string(),
41 status_code: 500,
42 }
43 }
44}
45
46pub fn is_enhanced_format(data: &serde_json::Value) -> bool {
48 data.get("format").and_then(|v| v.as_str()) == Some("enhanced")
49 && data.get("modules").is_some()
50 && data.get("references").is_some()
51}
52
53pub fn load_blueprint(ontology_path: &Path) -> Result<serde_json::Value, ApiError> {
55 let content = fs::read_to_string(ontology_path)
56 .map_err(|e| ApiError::internal(&format!("读取文件失败: {}", e)))?;
57 serde_json::from_str(&content)
58 .map_err(|e| ApiError::internal(&format!("解析 JSON 失败: {}", e)))
59}
60
61pub fn load_enhanced_blueprint(ontology_path: &Path) -> Result<EnhancedCodeBlueprint, ApiError> {
63 let content = fs::read_to_string(ontology_path)
64 .map_err(|e| ApiError::internal(&format!("读取文件失败: {}", e)))?;
65 serde_json::from_str(&content).map_err(|e| ApiError::internal(&format!("解析蓝图失败: {}", e)))
66}
67
68pub fn infer_map_dir(ontology_path: &Path) -> PathBuf {
70 if ontology_path
71 .extension()
72 .map(|e| e == "json")
73 .unwrap_or(false)
74 {
75 ontology_path
76 .parent()
77 .unwrap_or(Path::new("."))
78 .join(".claude/map")
79 } else {
80 ontology_path.to_path_buf()
81 }
82}
83
84pub struct ApiHandlers {
86 ontology_path: PathBuf,
87 map_dir: PathBuf,
88}
89
90impl ApiHandlers {
91 pub fn new(ontology_path: PathBuf) -> Self {
92 let map_dir = infer_map_dir(&ontology_path);
93 Self {
94 ontology_path,
95 map_dir,
96 }
97 }
98
99 pub fn get_ontology(&self) -> Result<serde_json::Value, ApiError> {
101 let index_path = self.map_dir.join("index.json");
102 if index_path.exists() {
103 let content =
104 fs::read_to_string(&index_path).map_err(|e| ApiError::internal(&e.to_string()))?;
105 serde_json::from_str(&content).map_err(|e| ApiError::internal(&e.to_string()))
106 } else {
107 Err(ApiError::not_found(
108 "Blueprint not found. Please run /map generate first.",
109 ))
110 }
111 }
112
113 pub fn get_chunk(&self, chunk_path: &str) -> Result<serde_json::Value, ApiError> {
115 if chunk_path.contains("..") || chunk_path.contains('~') {
117 return Err(ApiError::bad_request("Invalid chunk path"));
118 }
119
120 let chunk_file = self
121 .map_dir
122 .join("chunks")
123 .join(format!("{}.json", chunk_path));
124 if !chunk_file.exists() {
125 return Err(ApiError::not_found(&format!(
126 "Chunk not found: {}",
127 chunk_path
128 )));
129 }
130
131 let content =
132 fs::read_to_string(&chunk_file).map_err(|e| ApiError::internal(&e.to_string()))?;
133 serde_json::from_str(&content).map_err(|e| ApiError::internal(&e.to_string()))
134 }
135
136 pub fn get_architecture(&self) -> Result<ArchitectureMap, ApiError> {
138 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
139 Ok(build_architecture_map(&blueprint))
140 }
141
142 pub fn get_entry_points(&self) -> Result<EntryPointsResponse, ApiError> {
144 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
145 let entries = detect_entry_points(&blueprint);
146 Ok(EntryPointsResponse {
147 entry_points: entries,
148 })
149 }
150
151 pub fn get_dependency_tree(
153 &self,
154 entry_id: &str,
155 max_depth: usize,
156 ) -> Result<DependencyTreeNode, ApiError> {
157 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
158 build_dependency_tree(&blueprint, entry_id, max_depth)
159 .ok_or_else(|| ApiError::not_found("Entry module not found"))
160 }
161
162 pub fn get_module_detail(&self, module_id: &str) -> Result<ModuleDetailInfo, ApiError> {
164 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
165 get_module_detail(&blueprint, module_id)
166 .ok_or_else(|| ApiError::not_found("Module not found"))
167 }
168
169 pub fn get_symbol_refs(&self, symbol_id: &str) -> Result<SymbolRefInfo, ApiError> {
171 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
172 get_symbol_refs(&blueprint, symbol_id)
173 .ok_or_else(|| ApiError::not_found("Symbol not found"))
174 }
175
176 pub fn search(&self, query: &str) -> Result<SearchResponse, ApiError> {
178 if query.is_empty() {
179 return Ok(SearchResponse {
180 results: Vec::new(),
181 });
182 }
183
184 let query_lower = query.to_lowercase();
185 let blueprint = load_enhanced_blueprint(&self.ontology_path)?;
186 let mut results: Vec<SearchResultItem> = Vec::new();
187
188 for module in blueprint.modules.values() {
190 if module.name.to_lowercase().contains(&query_lower)
191 || module.id.to_lowercase().contains(&query_lower)
192 {
193 results.push(SearchResultItem {
194 result_type: "module".to_string(),
195 id: module.id.clone(),
196 name: module.name.clone(),
197 module_id: None,
198 description: module.semantic.as_ref().map(|s| s.description.clone()),
199 });
200 }
201 }
202
203 for symbol in blueprint.symbols.values() {
205 if symbol.name.to_lowercase().contains(&query_lower) {
206 let kind_str = format!("{:?}", symbol.kind).to_lowercase();
207 results.push(SearchResultItem {
208 result_type: kind_str,
209 id: symbol.id.clone(),
210 name: symbol.name.clone(),
211 module_id: Some(symbol.module_id.clone()),
212 description: symbol.semantic.as_ref().map(|s| s.description.clone()),
213 });
214 }
215 }
216
217 results.truncate(50);
218 Ok(SearchResponse { results })
219 }
220
221 pub fn get_all_chunk_metadata(&self) -> Result<HashMap<String, ChunkMetadata>, ApiError> {
223 let chunks_dir = self.map_dir.join("chunks");
224 if !chunks_dir.exists() {
225 return Ok(HashMap::new());
226 }
227
228 let mut metadata: HashMap<String, ChunkMetadata> = HashMap::new();
229
230 let entries = fs::read_dir(&chunks_dir).map_err(|e| ApiError::internal(&e.to_string()))?;
231
232 for entry in entries.flatten() {
233 let path = entry.path();
234 if path.extension().map(|e| e == "json").unwrap_or(false) {
235 if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
236 let dir_path = if file_name == "root" {
237 String::new()
238 } else {
239 file_name.replace('_', "/")
240 };
241
242 if let Ok(meta) = fs::metadata(&path) {
243 let modified = meta
244 .modified()
245 .map(|t| {
246 t.duration_since(std::time::UNIX_EPOCH)
247 .map(|d| d.as_secs())
248 .unwrap_or(0)
249 })
250 .unwrap_or(0);
251
252 metadata.insert(
253 dir_path,
254 ChunkMetadata {
255 file: format!("chunks/{}.json", file_name),
256 last_modified: modified,
257 size: meta.len(),
258 checksum: format!("{}-{}", meta.len(), modified),
259 },
260 );
261 }
262 }
263 }
264 }
265
266 Ok(metadata)
267 }
268}
269
270#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct ChunkMetadata {
274 pub file: String,
275 pub last_modified: u64,
276 pub size: u64,
277 pub checksum: String,
278}