Skip to main content

feagi_api/endpoints/
morphology.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Morphology API Endpoints - Exact port from Python `/v1/morphology/*`
5
6// Removed - using crate::common::State instead
7use 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/// Get list of all morphology names in alphabetical order.
28#[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    // Sort morphology names alphabetically for consistent UI display
40    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/// Get available morphology types (vectors, patterns, projector).
49#[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/// Get morphologies categorized by type.
59#[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    // TODO: Get actual morphology categorization
64    // Use BTreeMap for alphabetical ordering in UI
65    Ok(Json(BTreeMap::new()))
66}
67
68/// Get all morphology definitions with their complete configurations.
69#[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    // Get morphologies from connectome
84    let morphologies = connectome_service
85        .get_morphologies()
86        .await
87        .map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
88
89    // Convert to Python-compatible format
90    // Use BTreeMap for alphabetical ordering in UI
91    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/// Create a new morphology definition.
109#[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            // BV payload wraps composite fields under {"composite": {...}}.
152            // Accept that exact schema (and also accept the direct flat schema).
153            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/// Update an existing morphology definition.
189#[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/// Delete a morphology by name provided in request body.
284#[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/// Rename a morphology and update all references in the genome.
311#[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/// Get detailed properties for a specific morphology by name.
355#[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    // Return properties in expected format
388    // Use BTreeMap for alphabetical ordering in UI
389    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/// Get all cortical area pairs that use a specific morphology.
409#[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    // Get all cortical areas
433    let areas = connectome_service
434        .list_cortical_areas()
435        .await
436        .map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
437
438    // Find all [src, dst] pairs that use this morphology
439    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/// Get list of all morphology names.
471#[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    // Sort morphology names alphabetically for consistent UI display
488    let mut names: Vec<String> = morphologies.keys().cloned().collect();
489    names.sort();
490    Ok(Json(names))
491}
492
493/// Get detailed information about a specific morphology using path parameter.
494#[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    // Delegate to post_morphology_properties (same logic)
510    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/// Create a new morphology with specified parameters.
521#[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    // TODO: Implement morphology creation
534    Ok(Json(HashMap::from([(
535        "message".to_string(),
536        "Morphology creation not yet implemented".to_string(),
537    )])))
538}
539
540/// Update an existing morphology's parameters.
541#[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    // TODO: Implement morphology update
554    Ok(Json(HashMap::from([(
555        "message".to_string(),
556        "Morphology update not yet implemented".to_string(),
557    )])))
558}
559
560/// Delete a morphology using path parameter.
561#[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    // TODO: Implement morphology deletion
577    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}