pub mod handlers;
use axum::Router;
use axum::routing::get;
use axum::routing::post;
use crate::state::EdgeState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Plane {
#[default]
Both,
Data,
Credential,
}
pub const PLANE_ENV: &str = "BRAIN_EDGE_PLANE";
impl Plane {
pub fn from_name(name: &str) -> Result<Self, String> {
match name.trim().to_ascii_lowercase().as_str() {
"both" | "" => Ok(Self::Both),
"data" => Ok(Self::Data),
"credential" => Ok(Self::Credential),
other => Err(format!(
"unknown plane `{other}`: expected one of both, data, credential"
)),
}
}
pub fn from_env() -> Result<Self, String> {
std::env::var(PLANE_ENV).map_or(Ok(Self::Both), |v| Self::from_name(&v))
}
#[must_use]
pub const fn serves_data(self) -> bool {
matches!(self, Self::Both | Self::Data)
}
#[must_use]
pub const fn serves_credentials(self) -> bool {
matches!(self, Self::Both | Self::Credential)
}
}
pub fn router(state: EdgeState) -> Router {
router_for(state, Plane::Both)
}
pub fn router_for(state: EdgeState, plane: Plane) -> Router {
let mut r = Router::new();
if plane.serves_credentials() {
r = r.merge(credential_router());
}
if plane.serves_data() {
r = r.merge(data_router());
}
r.with_state(state)
}
fn credential_router() -> Router<EdgeState> {
Router::new()
.route(
"/v1/llm-credential",
axum::routing::put(handlers::llm_credential::put)
.get(handlers::llm_credential::get)
.delete(handlers::llm_credential::delete),
)
.route(
"/llm/ns/{namespace}/openai/v1/chat/completions",
post(handlers::llm_proxy::openai_chat_completions),
)
.route(
"/llm/ns/{namespace}/anthropic/v1/messages",
post(handlers::llm_proxy::anthropic_messages),
)
.route(
"/llm/ns/{namespace}/resolve",
get(handlers::llm_proxy::resolve),
)
}
fn data_router() -> Router<EdgeState> {
Router::new()
.route("/v1/whoami", get(handlers::identity::whoami))
.route("/v1/capabilities", get(handlers::identity::capabilities))
.route(
"/v1/memories",
post(handlers::memory::encode)
.get(handlers::memory::list)
.delete(handlers::memory::forget),
)
.route("/v1/memories/{id}/inspect", get(handlers::memory::inspect))
.route("/v1/recall", post(handlers::memory::recall))
.route("/v1/plan", post(handlers::reasoning::plan))
.route("/v1/reason", post(handlers::reasoning::reason))
.route(
"/v1/links",
post(handlers::graph::link).delete(handlers::graph::unlink),
)
.route("/v1/graph", get(handlers::graph::fetch))
.route(
"/v1/entities",
post(handlers::entity::create).get(handlers::entity::list),
)
.route("/v1/entities/resolve", post(handlers::entity::resolve))
.route("/v1/entities/{id}", get(handlers::entity::get))
.route(
"/v1/entities/{id}/traverse",
post(handlers::entity::traverse),
)
.route("/v1/entities/{id}/relations", get(handlers::relation::list))
.route("/v1/statements", get(handlers::statement::list))
.route("/v1/statements/{id}", get(handlers::statement::get))
.route("/v1/relations/{id}", get(handlers::relation::get))
.route(
"/v1/schema",
get(handlers::schema::get)
.post(handlers::schema::upload)
.put(handlers::schema::replace),
)
.route("/v1/schema/validate", post(handlers::schema::validate))
}
#[cfg(test)]
mod plane_tests {
use super::*;
#[test]
fn the_default_serves_everything() {
assert_eq!(Plane::default(), Plane::Both);
assert!(Plane::Both.serves_data());
assert!(Plane::Both.serves_credentials());
assert_eq!(Plane::from_name("").unwrap(), Plane::Both);
}
#[test]
fn the_data_plane_cannot_reach_key_material() {
assert!(Plane::Data.serves_data());
assert!(!Plane::Data.serves_credentials());
}
#[test]
fn the_credential_plane_serves_no_memory_verbs() {
assert!(Plane::Credential.serves_credentials());
assert!(!Plane::Credential.serves_data());
}
#[test]
fn parsing_is_case_and_whitespace_tolerant() {
assert_eq!(Plane::from_name(" DATA ").unwrap(), Plane::Data);
assert_eq!(Plane::from_name("Credential").unwrap(), Plane::Credential);
}
#[test]
fn an_unknown_plane_is_an_error_not_a_silent_both() {
let err = Plane::from_name("dataplane").unwrap_err();
assert!(err.contains("dataplane"), "{err}");
assert!(err.contains("data"), "names the accepted set: {err}");
}
}