use crate::common::ApiState;
use crate::common::{ApiError, ApiResult, Json, Path, State};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
const DEFAULT_MORPHOLOGY_CLASS: &str = "custom";
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct MorphologyListResponse {
pub morphology_list: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RenameMorphologyRequest {
#[serde(alias = "old_morphology_name")]
pub old_morphology_id: String,
#[serde(alias = "new_morphology_name")]
pub new_morphology_id: String,
}
#[utoipa::path(get, path = "/v1/morphology/morphology_list", tag = "morphology")]
pub async fn get_morphology_list(
State(state): State<ApiState>,
) -> ApiResult<Json<MorphologyListResponse>> {
let connectome_service = state.connectome_service.as_ref();
let morphologies = connectome_service
.get_morphologies()
.await
.map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
let mut names: Vec<String> = morphologies.keys().cloned().collect();
names.sort();
Ok(Json(MorphologyListResponse {
morphology_list: names,
}))
}
#[utoipa::path(get, path = "/v1/morphology/morphology_types", tag = "morphology")]
pub async fn get_morphology_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
Ok(Json(vec![
"vectors".to_string(),
"patterns".to_string(),
"projector".to_string(),
]))
}
#[utoipa::path(get, path = "/v1/morphology/list/types", tag = "morphology")]
pub async fn get_list_types(
State(_state): State<ApiState>,
) -> ApiResult<Json<BTreeMap<String, Vec<String>>>> {
Ok(Json(BTreeMap::new()))
}
#[utoipa::path(
get,
path = "/v1/morphology/morphologies",
tag = "morphology",
responses(
(status = 200, description = "All morphology definitions", body = HashMap<String, serde_json::Value>),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_morphologies(
State(state): State<ApiState>,
) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
let connectome_service = state.connectome_service.as_ref();
let morphologies = connectome_service
.get_morphologies()
.await
.map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
let mut result = BTreeMap::new();
for (name, morphology_info) in morphologies.iter() {
result.insert(
name.clone(),
serde_json::json!({
"name": name,
"type": morphology_info.morphology_type,
"class": morphology_info.class,
"parameters": morphology_info.parameters,
"source": "genome"
}),
);
}
Ok(Json(result))
}
#[utoipa::path(post, path = "/v1/morphology/morphology", tag = "morphology")]
pub async fn post_morphology(
State(state): State<ApiState>,
Json(req): Json<HashMap<String, serde_json::Value>>,
) -> ApiResult<Json<HashMap<String, String>>> {
let morphology_name = req
.get("morphology_name")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
.trim()
.to_string();
if morphology_name.is_empty() {
return Err(ApiError::invalid_input("morphology_name must be non-empty"));
}
let morphology_type = req
.get("morphology_type")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::invalid_input("Missing morphology_type"))?
.trim()
.to_lowercase();
let morphology_parameters = req
.get("morphology_parameters")
.cloned()
.ok_or_else(|| ApiError::invalid_input("Missing morphology_parameters"))?;
let (morphology_type_enum, params_value) = match morphology_type.as_str() {
"vectors" => (
feagi_evolutionary::MorphologyType::Vectors,
morphology_parameters,
),
"patterns" => (
feagi_evolutionary::MorphologyType::Patterns,
morphology_parameters,
),
"functions" => (
feagi_evolutionary::MorphologyType::Functions,
morphology_parameters,
),
"composite" => {
let composite_obj = morphology_parameters
.get("composite")
.cloned()
.unwrap_or(morphology_parameters);
(feagi_evolutionary::MorphologyType::Composite, composite_obj)
}
other => {
return Err(ApiError::invalid_input(format!(
"Unknown morphology_type '{}'",
other
)))
}
};
let parameters: feagi_evolutionary::MorphologyParameters = serde_json::from_value(params_value)
.map_err(|e| ApiError::invalid_input(format!("Invalid morphology_parameters: {}", e)))?;
let morphology = feagi_evolutionary::Morphology {
morphology_type: morphology_type_enum,
parameters,
class: DEFAULT_MORPHOLOGY_CLASS.to_string(),
};
state
.connectome_service
.create_morphology(morphology_name, morphology)
.await
.map_err(ApiError::from)?;
Ok(Json(HashMap::from([(
"status".to_string(),
"success".to_string(),
)])))
}
#[utoipa::path(put, path = "/v1/morphology/morphology", tag = "morphology")]
pub async fn put_morphology(
State(state): State<ApiState>,
Json(req): Json<HashMap<String, serde_json::Value>>,
) -> ApiResult<Json<HashMap<String, String>>> {
tracing::info!(
target: "feagi-api",
"[MORPH-AUDIT][API] PUT /v1/morphology/morphology received payload keys={:?}",
req.keys().collect::<Vec<_>>()
);
let morphology_name = req
.get("morphology_name")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
.trim()
.to_string();
if morphology_name.is_empty() {
return Err(ApiError::invalid_input("morphology_name must be non-empty"));
}
let morphology_type = req
.get("morphology_type")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::invalid_input("Missing morphology_type"))?
.trim()
.to_lowercase();
let morphology_parameters = req
.get("morphology_parameters")
.cloned()
.ok_or_else(|| ApiError::invalid_input("Missing morphology_parameters"))?;
let (morphology_type_enum, params_value) = match morphology_type.as_str() {
"vectors" => (
feagi_evolutionary::MorphologyType::Vectors,
morphology_parameters,
),
"patterns" => (
feagi_evolutionary::MorphologyType::Patterns,
morphology_parameters,
),
"functions" => (
feagi_evolutionary::MorphologyType::Functions,
morphology_parameters,
),
"composite" => {
let composite_obj = morphology_parameters
.get("composite")
.cloned()
.unwrap_or(morphology_parameters);
(feagi_evolutionary::MorphologyType::Composite, composite_obj)
}
other => {
return Err(ApiError::invalid_input(format!(
"Unknown morphology_type '{}'",
other
)))
}
};
let parameters: feagi_evolutionary::MorphologyParameters = serde_json::from_value(params_value)
.map_err(|e| ApiError::invalid_input(format!("Invalid morphology_parameters: {}", e)))?;
let morphology = feagi_evolutionary::Morphology {
morphology_type: morphology_type_enum,
parameters,
class: DEFAULT_MORPHOLOGY_CLASS.to_string(),
};
tracing::info!(
target: "feagi-api",
"[MORPH-AUDIT][API] Dispatching update_morphology name={} type={}",
morphology_name,
morphology_type
);
state
.connectome_service
.update_morphology(morphology_name, morphology)
.await
.map_err(ApiError::from)?;
tracing::info!(
target: "feagi-api",
"[MORPH-AUDIT][API] update_morphology completed successfully"
);
Ok(Json(HashMap::from([(
"status".to_string(),
"success".to_string(),
)])))
}
#[utoipa::path(delete, path = "/v1/morphology/morphology", tag = "morphology")]
pub async fn delete_morphology_by_name(
State(state): State<ApiState>,
Json(req): Json<HashMap<String, String>>,
) -> ApiResult<Json<HashMap<String, String>>> {
let morphology_name = req
.get("morphology_name")
.ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?
.trim();
if morphology_name.is_empty() {
return Err(ApiError::invalid_input("morphology_name must be non-empty"));
}
state
.connectome_service
.delete_morphology(morphology_name)
.await
.map_err(ApiError::from)?;
Ok(Json(HashMap::from([(
"status".to_string(),
"success".to_string(),
)])))
}
#[utoipa::path(
put,
path = "/v1/morphology/rename",
tag = "morphology",
request_body = RenameMorphologyRequest,
responses(
(status = 200, description = "Morphology renamed", body = HashMap<String, String>),
(status = 404, description = "Morphology not found"),
(status = 409, description = "New morphology ID already exists"),
(status = 500, description = "Internal server error")
)
)]
pub async fn put_rename_morphology(
State(state): State<ApiState>,
Json(req): Json<RenameMorphologyRequest>,
) -> ApiResult<Json<HashMap<String, String>>> {
let old_id = req.old_morphology_id.trim();
let new_id = req.new_morphology_id.trim();
if old_id.is_empty() {
return Err(ApiError::invalid_input(
"old_morphology_id must be non-empty",
));
}
if new_id.is_empty() {
return Err(ApiError::invalid_input(
"new_morphology_id must be non-empty",
));
}
state
.connectome_service
.rename_morphology(old_id, new_id)
.await
.map_err(ApiError::from)?;
Ok(Json(HashMap::from([
("status".to_string(), "success".to_string()),
("old_morphology_id".to_string(), old_id.to_string()),
("new_morphology_id".to_string(), new_id.to_string()),
])))
}
#[utoipa::path(
post,
path = "/v1/morphology/morphology_properties",
tag = "morphology",
responses(
(status = 200, description = "Morphology properties", body = HashMap<String, serde_json::Value>),
(status = 404, description = "Morphology not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn post_morphology_properties(
State(state): State<ApiState>,
Json(req): Json<HashMap<String, String>>,
) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
use tracing::debug;
let morphology_name = req
.get("morphology_name")
.ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
debug!(target: "feagi-api", "Getting properties for morphology: {}", morphology_name);
let connectome_service = state.connectome_service.as_ref();
let morphologies = connectome_service
.get_morphologies()
.await
.map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
let morphology_info = morphologies
.get(morphology_name)
.ok_or_else(|| ApiError::not_found("Morphology", morphology_name))?;
let mut result = BTreeMap::new();
result.insert(
"morphology_name".to_string(),
serde_json::json!(morphology_name),
);
result.insert(
"type".to_string(),
serde_json::json!(morphology_info.morphology_type),
);
result.insert(
"class".to_string(),
serde_json::json!(morphology_info.class),
);
result.insert("parameters".to_string(), morphology_info.parameters.clone());
result.insert("source".to_string(), serde_json::json!("genome"));
Ok(Json(result))
}
#[utoipa::path(
post,
path = "/v1/morphology/morphology_usage",
tag = "morphology",
responses(
(status = 200, description = "Morphology usage pairs", body = Vec<Vec<String>>),
(status = 500, description = "Internal server error")
)
)]
pub async fn post_morphology_usage(
State(state): State<ApiState>,
Json(req): Json<HashMap<String, String>>,
) -> ApiResult<Json<Vec<Vec<String>>>> {
use tracing::debug;
let morphology_name = req
.get("morphology_name")
.ok_or_else(|| ApiError::invalid_input("Missing morphology_name"))?;
debug!(target: "feagi-api", "Getting usage for morphology: {}", morphology_name);
let connectome_service = state.connectome_service.as_ref();
let areas = connectome_service
.list_cortical_areas()
.await
.map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
let mut usage_pairs = Vec::new();
for area_info in areas {
if let Some(mapping_dst) = area_info.properties.get("cortical_mapping_dst") {
if let Some(dst_map) = mapping_dst.as_object() {
for (dst_id, connections) in dst_map {
if let Some(conn_array) = connections.as_array() {
for conn in conn_array {
let morph_id = if let Some(arr) = conn.as_array() {
arr.first().and_then(|v| v.as_str())
} else if let Some(obj) = conn.as_object() {
obj.get("morphology_id").and_then(|v| v.as_str())
} else {
None
};
if morph_id == Some(morphology_name.as_str()) {
usage_pairs
.push(vec![area_info.cortical_id.clone(), dst_id.clone()]);
}
}
}
}
}
}
}
debug!(target: "feagi-api", "Found {} usage pairs for morphology: {}", usage_pairs.len(), morphology_name);
Ok(Json(usage_pairs))
}
#[utoipa::path(
get,
path = "/v1/morphology/list",
tag = "morphology",
responses(
(status = 200, description = "List of morphology names", body = Vec<String>)
)
)]
pub async fn get_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
let connectome_service = state.connectome_service.as_ref();
let morphologies = connectome_service
.get_morphologies()
.await
.map_err(|e| ApiError::internal(format!("Failed to get morphologies: {}", e)))?;
let mut names: Vec<String> = morphologies.keys().cloned().collect();
names.sort();
Ok(Json(names))
}
#[utoipa::path(
get,
path = "/v1/morphology/info/{morphology_id}",
tag = "morphology",
params(
("morphology_id" = String, Path, description = "Morphology name")
),
responses(
(status = 200, description = "Morphology info", body = BTreeMap<String, serde_json::Value>)
)
)]
pub async fn get_info(
State(state): State<ApiState>,
Path(morphology_id): Path<String>,
) -> ApiResult<Json<BTreeMap<String, serde_json::Value>>> {
post_morphology_properties(
State(state),
Json(HashMap::from([(
"morphology_name".to_string(),
morphology_id,
)])),
)
.await
}
#[utoipa::path(
post,
path = "/v1/morphology/create",
tag = "morphology",
responses(
(status = 200, description = "Morphology created", body = HashMap<String, String>)
)
)]
pub async fn post_create(
State(_state): State<ApiState>,
Json(_request): Json<HashMap<String, serde_json::Value>>,
) -> ApiResult<Json<HashMap<String, String>>> {
Ok(Json(HashMap::from([(
"message".to_string(),
"Morphology creation not yet implemented".to_string(),
)])))
}
#[utoipa::path(
put,
path = "/v1/morphology/update",
tag = "morphology",
responses(
(status = 200, description = "Morphology updated", body = HashMap<String, String>)
)
)]
pub async fn put_update(
State(_state): State<ApiState>,
Json(_request): Json<HashMap<String, serde_json::Value>>,
) -> ApiResult<Json<HashMap<String, String>>> {
Ok(Json(HashMap::from([(
"message".to_string(),
"Morphology update not yet implemented".to_string(),
)])))
}
#[utoipa::path(
delete,
path = "/v1/morphology/delete/{morphology_id}",
tag = "morphology",
params(
("morphology_id" = String, Path, description = "Morphology name")
),
responses(
(status = 200, description = "Morphology deleted", body = HashMap<String, String>)
)
)]
pub async fn delete_morphology(
State(_state): State<ApiState>,
Path(morphology_id): Path<String>,
) -> ApiResult<Json<HashMap<String, String>>> {
tracing::info!(target: "feagi-api", "Delete morphology requested: {}", morphology_id);
Ok(Json(HashMap::from([(
"message".to_string(),
format!("Morphology {} deletion not yet implemented", morphology_id),
)])))
}