1use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, Path, State};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, HashMap};
11
12const DEFAULT_MORPHOLOGY_CLASS: &str = "custom";
13
14#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
15pub struct MorphologyListResponse {
16 pub morphology_list: Vec<String>,
17}
18
19#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
20pub struct RenameMorphologyRequest {
21 #[serde(alias = "old_morphology_name")]
22 pub old_morphology_id: String,
23 #[serde(alias = "new_morphology_name")]
24 pub new_morphology_id: String,
25}
26
27#[utoipa::path(get, path = "/v1/morphology/morphology_list", tag = "morphology")]
29pub async fn get_morphology_list(
30 State(state): State<ApiState>,
31) -> ApiResult<Json<MorphologyListResponse>> {
32 let connectome_service = state.connectome_service.as_ref();
33
34 let morphologies = connectome_service
35 .get_morphologies()
36 .await
37 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
38
39 let mut names: Vec<String> = morphologies.keys().cloned().collect();
41 names.sort();
42
43 Ok(Json(MorphologyListResponse {
44 morphology_list: names,
45 }))
46}
47
48#[utoipa::path(get, path = "/v1/morphology/morphology_types", tag = "morphology")]
50pub async fn get_morphology_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
51 Ok(Json(vec![
52 "vectors".to_string(),
53 "patterns".to_string(),
54 "projector".to_string(),
55 ]))
56}
57
58#[utoipa::path(get, path = "/v1/morphology/list/types", tag = "morphology")]
60pub async fn get_list_types(
61 State(_state): State<ApiState>,
62) -> ApiResult<Json<BTreeMap<String, Vec<String>>>> {
63 Ok(Json(BTreeMap::new()))
66}
67
68#[utoipa::path(
70 get,
71 path = "/v1/morphology/morphologies",
72 tag = "morphology",
73 responses(
74 (status = 200, description = "All morphology definitions", body = HashMap<String, serde_json::Value>),
75 (status = 500, description = "Internal server error")
76 )
77)]
78pub async fn get_morphologies(
79 State(state): State<ApiState>,
80) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
81 let connectome_service = state.connectome_service.as_ref();
82
83 let morphologies = connectome_service
85 .get_morphologies()
86 .await
87 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
88
89 let mut result = BTreeMap::new();
92 for (name, morphology_info) in morphologies.iter() {
93 result.insert(
94 name.clone(),
95 serde_json::json!({
96 "name": name,
97 "type": morphology_info.morphology_type,
98 "class": morphology_info.class,
99 "parameters": morphology_info.parameters,
100 "source": "genome"
101 }),
102 );
103 }
104
105 Ok(Json(result))
106}
107
108#[utoipa::path(post, path = "/v1/morphology/morphology", tag = "morphology")]
110pub async fn post_morphology(
111 State(state): State<ApiState>,
112 Json(req): Json<HashMap<String, serde_json::Value>>,
113) -> ApiResult<Json<HashMap<String, String>>> {
114 let morphology_name = req
115 .get("morphology_name")
116 .and_then(|v| v.as_str())
117 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
118 .trim()
119 .to_string();
120
121 if morphology_name.is_empty() {
122 return Err(ApiError::invalid_input("morphology_name must be non-empty"));
123 }
124
125 let morphology_type = req
126 .get("morphology_type")
127 .and_then(|v| v.as_str())
128 .ok_or_else(|| ApiError::invalid_input("Missing morphology_type"))?
129 .trim()
130 .to_lowercase();
131
132 let morphology_parameters = req
133 .get("morphology_parameters")
134 .cloned()
135 .ok_or_else(|| ApiError::invalid_input("Missing morphology_parameters"))?;
136
137 let (morphology_type_enum, params_value) = match morphology_type.as_str() {
138 "vectors" => (
139 feagi_evolutionary::MorphologyType::Vectors,
140 morphology_parameters,
141 ),
142 "patterns" => (
143 feagi_evolutionary::MorphologyType::Patterns,
144 morphology_parameters,
145 ),
146 "functions" => (
147 feagi_evolutionary::MorphologyType::Functions,
148 morphology_parameters,
149 ),
150 "composite" => {
151 let composite_obj = morphology_parameters
154 .get("composite")
155 .cloned()
156 .unwrap_or(morphology_parameters);
157 (feagi_evolutionary::MorphologyType::Composite, composite_obj)
158 }
159 other => {
160 return Err(ApiError::invalid_input(format!(
161 "Unknown morphology_type '{}'",
162 other
163 )))
164 }
165 };
166
167 let parameters: feagi_evolutionary::MorphologyParameters = serde_json::from_value(params_value)
168 .map_err(|e| ApiError::invalid_input(format!("Invalid morphology_parameters: {}", e)))?;
169
170 let morphology = feagi_evolutionary::Morphology {
171 morphology_type: morphology_type_enum,
172 parameters,
173 class: DEFAULT_MORPHOLOGY_CLASS.to_string(),
174 };
175
176 state
177 .connectome_service
178 .create_morphology(morphology_name, morphology)
179 .await
180 .map_err(ApiError::from)?;
181
182 Ok(Json(HashMap::from([(
183 "status".to_string(),
184 "success".to_string(),
185 )])))
186}
187
188#[utoipa::path(put, path = "/v1/morphology/morphology", tag = "morphology")]
190pub async fn put_morphology(
191 State(state): State<ApiState>,
192 Json(req): Json<HashMap<String, serde_json::Value>>,
193) -> ApiResult<Json<HashMap<String, String>>> {
194 tracing::info!(
195 target: "feagi-api",
196 "[MORPH-AUDIT][API] PUT /v1/morphology/morphology received payload keys={:?}",
197 req.keys().collect::<Vec<_>>()
198 );
199 let morphology_name = req
200 .get("morphology_name")
201 .and_then(|v| v.as_str())
202 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
203 .trim()
204 .to_string();
205
206 if morphology_name.is_empty() {
207 return Err(ApiError::invalid_input("morphology_name must be non-empty"));
208 }
209
210 let morphology_type = req
211 .get("morphology_type")
212 .and_then(|v| v.as_str())
213 .ok_or_else(|| ApiError::invalid_input("Missing morphology_type"))?
214 .trim()
215 .to_lowercase();
216
217 let morphology_parameters = req
218 .get("morphology_parameters")
219 .cloned()
220 .ok_or_else(|| ApiError::invalid_input("Missing morphology_parameters"))?;
221
222 let (morphology_type_enum, params_value) = match morphology_type.as_str() {
223 "vectors" => (
224 feagi_evolutionary::MorphologyType::Vectors,
225 morphology_parameters,
226 ),
227 "patterns" => (
228 feagi_evolutionary::MorphologyType::Patterns,
229 morphology_parameters,
230 ),
231 "functions" => (
232 feagi_evolutionary::MorphologyType::Functions,
233 morphology_parameters,
234 ),
235 "composite" => {
236 let composite_obj = morphology_parameters
237 .get("composite")
238 .cloned()
239 .unwrap_or(morphology_parameters);
240 (feagi_evolutionary::MorphologyType::Composite, composite_obj)
241 }
242 other => {
243 return Err(ApiError::invalid_input(format!(
244 "Unknown morphology_type '{}'",
245 other
246 )))
247 }
248 };
249
250 let parameters: feagi_evolutionary::MorphologyParameters = serde_json::from_value(params_value)
251 .map_err(|e| ApiError::invalid_input(format!("Invalid morphology_parameters: {}", e)))?;
252
253 let morphology = feagi_evolutionary::Morphology {
254 morphology_type: morphology_type_enum,
255 parameters,
256 class: DEFAULT_MORPHOLOGY_CLASS.to_string(),
257 };
258
259 tracing::info!(
260 target: "feagi-api",
261 "[MORPH-AUDIT][API] Dispatching update_morphology name={} type={}",
262 morphology_name,
263 morphology_type
264 );
265
266 state
267 .connectome_service
268 .update_morphology(morphology_name, morphology)
269 .await
270 .map_err(ApiError::from)?;
271
272 tracing::info!(
273 target: "feagi-api",
274 "[MORPH-AUDIT][API] update_morphology completed successfully"
275 );
276
277 Ok(Json(HashMap::from([(
278 "status".to_string(),
279 "success".to_string(),
280 )])))
281}
282
283#[utoipa::path(delete, path = "/v1/morphology/morphology", tag = "morphology")]
285pub async fn delete_morphology_by_name(
286 State(state): State<ApiState>,
287 Json(req): Json<HashMap<String, String>>,
288) -> ApiResult<Json<HashMap<String, String>>> {
289 let morphology_name = req
290 .get("morphology_name")
291 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
292 .trim();
293
294 if morphology_name.is_empty() {
295 return Err(ApiError::invalid_input("morphology_name must be non-empty"));
296 }
297
298 state
299 .connectome_service
300 .delete_morphology(morphology_name)
301 .await
302 .map_err(ApiError::from)?;
303
304 Ok(Json(HashMap::from([(
305 "status".to_string(),
306 "success".to_string(),
307 )])))
308}
309
310#[utoipa::path(
312 put,
313 path = "/v1/morphology/rename",
314 tag = "morphology",
315 request_body = RenameMorphologyRequest,
316 responses(
317 (status = 200, description = "Morphology renamed", body = HashMap<String, String>),
318 (status = 404, description = "Morphology not found"),
319 (status = 409, description = "New morphology ID already exists"),
320 (status = 500, description = "Internal server error")
321 )
322)]
323pub async fn put_rename_morphology(
324 State(state): State<ApiState>,
325 Json(req): Json<RenameMorphologyRequest>,
326) -> ApiResult<Json<HashMap<String, String>>> {
327 let old_id = req.old_morphology_id.trim();
328 let new_id = req.new_morphology_id.trim();
329
330 if old_id.is_empty() {
331 return Err(ApiError::invalid_input(
332 "old_morphology_id must be non-empty",
333 ));
334 }
335 if new_id.is_empty() {
336 return Err(ApiError::invalid_input(
337 "new_morphology_id must be non-empty",
338 ));
339 }
340
341 state
342 .connectome_service
343 .rename_morphology(old_id, new_id)
344 .await
345 .map_err(ApiError::from)?;
346
347 Ok(Json(HashMap::from([
348 ("status".to_string(), "success".to_string()),
349 ("old_morphology_id".to_string(), old_id.to_string()),
350 ("new_morphology_id".to_string(), new_id.to_string()),
351 ])))
352}
353
354#[utoipa::path(
356 post,
357 path = "/v1/morphology/morphology_properties",
358 tag = "morphology",
359 responses(
360 (status = 200, description = "Morphology properties", body = HashMap<String, serde_json::Value>),
361 (status = 404, description = "Morphology not found"),
362 (status = 500, description = "Internal server error")
363 )
364)]
365pub async fn post_morphology_properties(
366 State(state): State<ApiState>,
367 Json(req): Json<HashMap<String, String>>,
368) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
369 use tracing::debug;
370
371 let morphology_name = req
372 .get("morphology_name")
373 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
374
375 debug!(target: "feagi-api", "Getting properties for morphology: {}", morphology_name);
376
377 let connectome_service = state.connectome_service.as_ref();
378 let morphologies = connectome_service
379 .get_morphologies()
380 .await
381 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
382
383 let morphology_info = morphologies
384 .get(morphology_name)
385 .ok_or_else(|| ApiError::not_found("Morphology", morphology_name))?;
386
387 let mut result = BTreeMap::new();
390 result.insert(
391 "morphology_name".to_string(),
392 serde_json::json!(morphology_name),
393 );
394 result.insert(
395 "type".to_string(),
396 serde_json::json!(morphology_info.morphology_type),
397 );
398 result.insert(
399 "class".to_string(),
400 serde_json::json!(morphology_info.class),
401 );
402 result.insert("parameters".to_string(), morphology_info.parameters.clone());
403 result.insert("source".to_string(), serde_json::json!("genome"));
404
405 Ok(Json(result))
406}
407
408#[utoipa::path(
410 post,
411 path = "/v1/morphology/morphology_usage",
412 tag = "morphology",
413 responses(
414 (status = 200, description = "Morphology usage pairs", body = Vec<Vec<String>>),
415 (status = 500, description = "Internal server error")
416 )
417)]
418pub async fn post_morphology_usage(
419 State(state): State<ApiState>,
420 Json(req): Json<HashMap<String, String>>,
421) -> ApiResult<Json<Vec<Vec<String>>>> {
422 use tracing::debug;
423
424 let morphology_name = req
425 .get("morphology_name")
426 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
427
428 debug!(target: "feagi-api", "Getting usage for morphology: {}", morphology_name);
429
430 let connectome_service = state.connectome_service.as_ref();
431
432 let areas = connectome_service
434 .list_cortical_areas()
435 .await
436 .map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
437
438 let mut usage_pairs = Vec::new();
440
441 for area_info in areas {
442 if let Some(mapping_dst) = area_info.properties.get("cortical_mapping_dst") {
443 if let Some(dst_map) = mapping_dst.as_object() {
444 for (dst_id, connections) in dst_map {
445 if let Some(conn_array) = connections.as_array() {
446 for conn in conn_array {
447 let morph_id = if let Some(arr) = conn.as_array() {
448 arr.first().and_then(|v| v.as_str())
449 } else if let Some(obj) = conn.as_object() {
450 obj.get("morphology_id").and_then(|v| v.as_str())
451 } else {
452 None
453 };
454
455 if morph_id == Some(morphology_name.as_str()) {
456 usage_pairs
457 .push(vec![area_info.cortical_id.clone(), dst_id.clone()]);
458 }
459 }
460 }
461 }
462 }
463 }
464 }
465
466 debug!(target: "feagi-api", "Found {} usage pairs for morphology: {}", usage_pairs.len(), morphology_name);
467 Ok(Json(usage_pairs))
468}
469
470#[utoipa::path(
472 get,
473 path = "/v1/morphology/list",
474 tag = "morphology",
475 responses(
476 (status = 200, description = "List of morphology names", body = Vec<String>)
477 )
478)]
479pub async fn get_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
480 let connectome_service = state.connectome_service.as_ref();
481
482 let morphologies = connectome_service
483 .get_morphologies()
484 .await
485 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
486
487 let mut names: Vec<String> = morphologies.keys().cloned().collect();
489 names.sort();
490 Ok(Json(names))
491}
492
493#[utoipa::path(
495 get,
496 path = "/v1/morphology/info/{morphology_id}",
497 tag = "morphology",
498 params(
499 ("morphology_id" = String, Path, description = "Morphology name")
500 ),
501 responses(
502 (status = 200, description = "Morphology info", body = BTreeMap<String, serde_json::Value>)
503 )
504)]
505pub async fn get_info(
506 State(state): State<ApiState>,
507 Path(morphology_id): Path<String>,
508) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
509 post_morphology_properties(
511 State(state),
512 Json(HashMap::from([(
513 "morphology_name".to_string(),
514 morphology_id,
515 )])),
516 )
517 .await
518}
519
520#[utoipa::path(
522 post,
523 path = "/v1/morphology/create",
524 tag = "morphology",
525 responses(
526 (status = 200, description = "Morphology created", body = HashMap<String, String>)
527 )
528)]
529pub async fn post_create(
530 State(_state): State<ApiState>,
531 Json(_request): Json<HashMap<String, serde_json::Value>>,
532) -> ApiResult<Json<HashMap<String, String>>> {
533 Ok(Json(HashMap::from([(
535 "message".to_string(),
536 "Morphology creation not yet implemented".to_string(),
537 )])))
538}
539
540#[utoipa::path(
542 put,
543 path = "/v1/morphology/update",
544 tag = "morphology",
545 responses(
546 (status = 200, description = "Morphology updated", body = HashMap<String, String>)
547 )
548)]
549pub async fn put_update(
550 State(_state): State<ApiState>,
551 Json(_request): Json<HashMap<String, serde_json::Value>>,
552) -> ApiResult<Json<HashMap<String, String>>> {
553 Ok(Json(HashMap::from([(
555 "message".to_string(),
556 "Morphology update not yet implemented".to_string(),
557 )])))
558}
559
560#[utoipa::path(
562 delete,
563 path = "/v1/morphology/delete/{morphology_id}",
564 tag = "morphology",
565 params(
566 ("morphology_id" = String, Path, description = "Morphology name")
567 ),
568 responses(
569 (status = 200, description = "Morphology deleted", body = HashMap<String, String>)
570 )
571)]
572pub async fn delete_morphology(
573 State(_state): State<ApiState>,
574 Path(morphology_id): Path<String>,
575) -> ApiResult<Json<HashMap<String, String>>> {
576 tracing::info!(target: "feagi-api", "Delete morphology requested: {}", morphology_id);
578
579 Ok(Json(HashMap::from([(
580 "message".to_string(),
581 format!("Morphology {} deletion not yet implemented", morphology_id),
582 )])))
583}