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 let morphology_name = req
195 .get("morphology_name")
196 .and_then(|v| v.as_str())
197 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
198 .trim()
199 .to_string();
200
201 if morphology_name.is_empty() {
202 return Err(ApiError::invalid_input("morphology_name must be non-empty"));
203 }
204
205 let morphology_type = req
206 .get("morphology_type")
207 .and_then(|v| v.as_str())
208 .ok_or_else(|| ApiError::invalid_input("Missing morphology_type"))?
209 .trim()
210 .to_lowercase();
211
212 let morphology_parameters = req
213 .get("morphology_parameters")
214 .cloned()
215 .ok_or_else(|| ApiError::invalid_input("Missing morphology_parameters"))?;
216
217 let (morphology_type_enum, params_value) = match morphology_type.as_str() {
218 "vectors" => (
219 feagi_evolutionary::MorphologyType::Vectors,
220 morphology_parameters,
221 ),
222 "patterns" => (
223 feagi_evolutionary::MorphologyType::Patterns,
224 morphology_parameters,
225 ),
226 "functions" => (
227 feagi_evolutionary::MorphologyType::Functions,
228 morphology_parameters,
229 ),
230 "composite" => {
231 let composite_obj = morphology_parameters
232 .get("composite")
233 .cloned()
234 .unwrap_or(morphology_parameters);
235 (feagi_evolutionary::MorphologyType::Composite, composite_obj)
236 }
237 other => {
238 return Err(ApiError::invalid_input(format!(
239 "Unknown morphology_type '{}'",
240 other
241 )))
242 }
243 };
244
245 let parameters: feagi_evolutionary::MorphologyParameters = serde_json::from_value(params_value)
246 .map_err(|e| ApiError::invalid_input(format!("Invalid morphology_parameters: {}", e)))?;
247
248 let morphology = feagi_evolutionary::Morphology {
249 morphology_type: morphology_type_enum,
250 parameters,
251 class: DEFAULT_MORPHOLOGY_CLASS.to_string(),
252 };
253
254 state
255 .connectome_service
256 .update_morphology(morphology_name, morphology)
257 .await
258 .map_err(ApiError::from)?;
259
260 Ok(Json(HashMap::from([(
261 "status".to_string(),
262 "success".to_string(),
263 )])))
264}
265
266#[utoipa::path(delete, path = "/v1/morphology/morphology", tag = "morphology")]
268pub async fn delete_morphology_by_name(
269 State(state): State<ApiState>,
270 Json(req): Json<HashMap<String, String>>,
271) -> ApiResult<Json<HashMap<String, String>>> {
272 let morphology_name = req
273 .get("morphology_name")
274 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
275 .trim();
276
277 if morphology_name.is_empty() {
278 return Err(ApiError::invalid_input("morphology_name must be non-empty"));
279 }
280
281 state
282 .connectome_service
283 .delete_morphology(morphology_name)
284 .await
285 .map_err(ApiError::from)?;
286
287 Ok(Json(HashMap::from([(
288 "status".to_string(),
289 "success".to_string(),
290 )])))
291}
292
293#[utoipa::path(
295 put,
296 path = "/v1/morphology/rename",
297 tag = "morphology",
298 request_body = RenameMorphologyRequest,
299 responses(
300 (status = 200, description = "Morphology renamed", body = HashMap<String, String>),
301 (status = 404, description = "Morphology not found"),
302 (status = 409, description = "New morphology ID already exists"),
303 (status = 500, description = "Internal server error")
304 )
305)]
306pub async fn put_rename_morphology(
307 State(state): State<ApiState>,
308 Json(req): Json<RenameMorphologyRequest>,
309) -> ApiResult<Json<HashMap<String, String>>> {
310 let old_id = req.old_morphology_id.trim();
311 let new_id = req.new_morphology_id.trim();
312
313 if old_id.is_empty() {
314 return Err(ApiError::invalid_input(
315 "old_morphology_id must be non-empty",
316 ));
317 }
318 if new_id.is_empty() {
319 return Err(ApiError::invalid_input(
320 "new_morphology_id must be non-empty",
321 ));
322 }
323
324 state
325 .connectome_service
326 .rename_morphology(old_id, new_id)
327 .await
328 .map_err(ApiError::from)?;
329
330 Ok(Json(HashMap::from([
331 ("status".to_string(), "success".to_string()),
332 ("old_morphology_id".to_string(), old_id.to_string()),
333 ("new_morphology_id".to_string(), new_id.to_string()),
334 ])))
335}
336
337#[utoipa::path(
339 post,
340 path = "/v1/morphology/morphology_properties",
341 tag = "morphology",
342 responses(
343 (status = 200, description = "Morphology properties", body = HashMap<String, serde_json::Value>),
344 (status = 404, description = "Morphology not found"),
345 (status = 500, description = "Internal server error")
346 )
347)]
348pub async fn post_morphology_properties(
349 State(state): State<ApiState>,
350 Json(req): Json<HashMap<String, String>>,
351) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
352 use tracing::debug;
353
354 let morphology_name = req
355 .get("morphology_name")
356 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
357
358 debug!(target: "feagi-api", "Getting properties for morphology: {}", morphology_name);
359
360 let connectome_service = state.connectome_service.as_ref();
361 let morphologies = connectome_service
362 .get_morphologies()
363 .await
364 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
365
366 let morphology_info = morphologies
367 .get(morphology_name)
368 .ok_or_else(|| ApiError::not_found("Morphology", morphology_name))?;
369
370 let mut result = BTreeMap::new();
373 result.insert(
374 "morphology_name".to_string(),
375 serde_json::json!(morphology_name),
376 );
377 result.insert(
378 "type".to_string(),
379 serde_json::json!(morphology_info.morphology_type),
380 );
381 result.insert(
382 "class".to_string(),
383 serde_json::json!(morphology_info.class),
384 );
385 result.insert("parameters".to_string(), morphology_info.parameters.clone());
386 result.insert("source".to_string(), serde_json::json!("genome"));
387
388 Ok(Json(result))
389}
390
391#[utoipa::path(
393 post,
394 path = "/v1/morphology/morphology_usage",
395 tag = "morphology",
396 responses(
397 (status = 200, description = "Morphology usage pairs", body = Vec<Vec<String>>),
398 (status = 500, description = "Internal server error")
399 )
400)]
401pub async fn post_morphology_usage(
402 State(state): State<ApiState>,
403 Json(req): Json<HashMap<String, String>>,
404) -> ApiResult<Json<Vec<Vec<String>>>> {
405 use tracing::debug;
406
407 let morphology_name = req
408 .get("morphology_name")
409 .ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
410
411 debug!(target: "feagi-api", "Getting usage for morphology: {}", morphology_name);
412
413 let connectome_service = state.connectome_service.as_ref();
414
415 let areas = connectome_service
417 .list_cortical_areas()
418 .await
419 .map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
420
421 let mut usage_pairs = Vec::new();
423
424 for area_info in areas {
425 if let Some(mapping_dst) = area_info.properties.get("cortical_mapping_dst") {
426 if let Some(dst_map) = mapping_dst.as_object() {
427 for (dst_id, connections) in dst_map {
428 if let Some(conn_array) = connections.as_array() {
429 for conn in conn_array {
430 let morph_id = if let Some(arr) = conn.as_array() {
431 arr.first().and_then(|v| v.as_str())
432 } else if let Some(obj) = conn.as_object() {
433 obj.get("morphology_id").and_then(|v| v.as_str())
434 } else {
435 None
436 };
437
438 if morph_id == Some(morphology_name.as_str()) {
439 usage_pairs
440 .push(vec![area_info.cortical_id.clone(), dst_id.clone()]);
441 }
442 }
443 }
444 }
445 }
446 }
447 }
448
449 debug!(target: "feagi-api", "Found {} usage pairs for morphology: {}", usage_pairs.len(), morphology_name);
450 Ok(Json(usage_pairs))
451}
452
453#[utoipa::path(
455 get,
456 path = "/v1/morphology/list",
457 tag = "morphology",
458 responses(
459 (status = 200, description = "List of morphology names", body = Vec<String>)
460 )
461)]
462pub async fn get_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
463 let connectome_service = state.connectome_service.as_ref();
464
465 let morphologies = connectome_service
466 .get_morphologies()
467 .await
468 .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
469
470 let mut names: Vec<String> = morphologies.keys().cloned().collect();
472 names.sort();
473 Ok(Json(names))
474}
475
476#[utoipa::path(
478 get,
479 path = "/v1/morphology/info/{morphology_id}",
480 tag = "morphology",
481 params(
482 ("morphology_id" = String, Path, description = "Morphology name")
483 ),
484 responses(
485 (status = 200, description = "Morphology info", body = BTreeMap<String, serde_json::Value>)
486 )
487)]
488pub async fn get_info(
489 State(state): State<ApiState>,
490 Path(morphology_id): Path<String>,
491) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
492 post_morphology_properties(
494 State(state),
495 Json(HashMap::from([(
496 "morphology_name".to_string(),
497 morphology_id,
498 )])),
499 )
500 .await
501}
502
503#[utoipa::path(
505 post,
506 path = "/v1/morphology/create",
507 tag = "morphology",
508 responses(
509 (status = 200, description = "Morphology created", body = HashMap<String, String>)
510 )
511)]
512pub async fn post_create(
513 State(_state): State<ApiState>,
514 Json(_request): Json<HashMap<String, serde_json::Value>>,
515) -> ApiResult<Json<HashMap<String, String>>> {
516 Ok(Json(HashMap::from([(
518 "message".to_string(),
519 "Morphology creation not yet implemented".to_string(),
520 )])))
521}
522
523#[utoipa::path(
525 put,
526 path = "/v1/morphology/update",
527 tag = "morphology",
528 responses(
529 (status = 200, description = "Morphology updated", body = HashMap<String, String>)
530 )
531)]
532pub async fn put_update(
533 State(_state): State<ApiState>,
534 Json(_request): Json<HashMap<String, serde_json::Value>>,
535) -> ApiResult<Json<HashMap<String, String>>> {
536 Ok(Json(HashMap::from([(
538 "message".to_string(),
539 "Morphology update not yet implemented".to_string(),
540 )])))
541}
542
543#[utoipa::path(
545 delete,
546 path = "/v1/morphology/delete/{morphology_id}",
547 tag = "morphology",
548 params(
549 ("morphology_id" = String, Path, description = "Morphology name")
550 ),
551 responses(
552 (status = 200, description = "Morphology deleted", body = HashMap<String, String>)
553 )
554)]
555pub async fn delete_morphology(
556 State(_state): State<ApiState>,
557 Path(morphology_id): Path<String>,
558) -> ApiResult<Json<HashMap<String, String>>> {
559 tracing::info!(target: "feagi-api", "Delete morphology requested: {}", morphology_id);
561
562 Ok(Json(HashMap::from([(
563 "message".to_string(),
564 format!("Morphology {} deletion not yet implemented", morphology_id),
565 )])))
566}