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    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/// Delete a morphology by name provided in request body.
267#[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/// Rename a morphology and update all references in the genome.
294#[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/// Get detailed properties for a specific morphology by name.
338#[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    // Return properties in expected format
371    // Use BTreeMap for alphabetical ordering in UI
372    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/// Get all cortical area pairs that use a specific morphology.
392#[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    // Get all cortical areas
416    let areas = connectome_service
417        .list_cortical_areas()
418        .await
419        .map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
420
421    // Find all [src, dst] pairs that use this morphology
422    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/// Get list of all morphology names.
454#[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    // Sort morphology names alphabetically for consistent UI display
471    let mut names: Vec<String> = morphologies.keys().cloned().collect();
472    names.sort();
473    Ok(Json(names))
474}
475
476/// Get detailed information about a specific morphology using path parameter.
477#[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    // Delegate to post_morphology_properties (same logic)
493    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/// Create a new morphology with specified parameters.
504#[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    // TODO: Implement morphology creation
517    Ok(Json(HashMap::from([(
518        "message".to_string(),
519        "Morphology creation not yet implemented".to_string(),
520    )])))
521}
522
523/// Update an existing morphology's parameters.
524#[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    // TODO: Implement morphology update
537    Ok(Json(HashMap::from([(
538        "message".to_string(),
539        "Morphology update not yet implemented".to_string(),
540    )])))
541}
542
543/// Delete a morphology using path parameter.
544#[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    // TODO: Implement morphology deletion
560    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}