use std::{
collections::{BTreeMap, BTreeSet},
sync::RwLock,
};
use fraiseql_core::capability::{
CapabilityMeta, CompiledSection, EntryPoints, Maturity, Release, SectionActivity,
};
use serde::Serialize;
use crate::{
routes::graphql::AppState,
server::{
capabilities::{self as impls, RunCx, ServeCx},
provision::{BootCx, ProvisionStep},
},
};
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Registration {
#[serde(flatten)]
pub meta: CapabilityMeta,
pub compiled_in: bool,
#[serde(skip)]
pub(crate) capability: &'static dyn Capability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Started {
Running,
#[cfg_attr(
not(any(feature = "observers", feature = "sources")),
allow(dead_code) // Reason: only those two capabilities can serve on without starting
)]
Degraded,
}
#[async_trait::async_trait]
pub(crate) trait Capability: Send + Sync + std::fmt::Debug {
fn configured(&self, _cx: &ServeCx<'_>) -> bool {
false
}
fn provision(&self, _cx: &BootCx<'_>) -> Vec<ProvisionStep> {
Vec::new()
}
async fn start(&self, _cx: &mut RunCx<'_>) -> crate::Result<Started> {
Ok(Started::Running)
}
fn routes(&self, _cx: &ServeCx<'_>) -> Option<axum::Router> {
None
}
async fn health(&self, _app: &AppState) -> Option<bool> {
None
}
}
macro_rules! register {
(
capability: $capability:expr,name: $name:literal,feature: $feature:literal,compiled_sections: $compiled:expr,runtime_keys:
$runtime:expr,docs: $docs:expr,maturity: $maturity:expr,release: $release:expr,entry_points:
$entry_points:expr $(,)?
) => {
Registration {
meta: CapabilityMeta {
name: $name,
feature: $feature,
compiled_sections: $compiled,
runtime_keys: $runtime,
docs: $docs,
maturity: $maturity,
release: $release,
entry_points: $entry_points,
},
compiled_in: cfg!(feature = $feature),
capability: &$capability,
}
};
}
const fn section(key: &'static str, activity: SectionActivity) -> CompiledSection {
CompiledSection { key, activity }
}
pub static REGISTRY: &[Registration] = &[
register! {
capability: impls::Functions,
name: "functions",
feature: "functions-runtime",
compiled_sections: &[section("functions", SectionActivity::NonEmptyDefinitions)],
runtime_keys: &[],
docs: Some("docs/architecture/functions.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::ALL,
},
register! {
capability: impls::Sources,
name: "sources",
feature: "sources",
compiled_sections: &[section("sources", SectionActivity::AnyEntryEnabled)],
runtime_keys: &["sources"],
docs: Some("docs/architecture/sources.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::Mcp,
name: "mcp",
feature: "mcp",
compiled_sections: &[section("mcp_config", SectionActivity::EnabledFlag)],
runtime_keys: &[],
docs: Some("docs/mcp.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::ALL,
},
register! {
capability: impls::Rest,
name: "rest",
feature: "rest",
compiled_sections: &[section("rest_config", SectionActivity::EnabledFlag)],
runtime_keys: &["export"],
docs: None,
maturity: Maturity::Stable,
release: Release::Full,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::Grpc,
name: "grpc",
feature: "grpc",
compiled_sections: &[section("grpc_config", SectionActivity::EnabledFlag)],
runtime_keys: &[],
docs: None,
maturity: Maturity::Preview,
release: Release::Source,
entry_points: EntryPoints::NONE,
},
register! {
capability: impls::Federation,
name: "federation",
feature: "federation",
compiled_sections: &[section("federation", SectionActivity::EnabledFlag)],
runtime_keys: &[],
docs: Some("docs/architecture/federation.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::Observers,
name: "observers",
feature: "observers",
compiled_sections: &[section("observers_config", SectionActivity::EnabledFlag)],
runtime_keys: &["observers"],
docs: Some("docs/operations/observers.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::ArrowFlight,
name: "arrow_flight",
feature: "arrow",
compiled_sections: &[],
runtime_keys: &["flight_bind_addr", "flight_upload_tables", "flight_views"],
docs: None,
maturity: Maturity::Stable,
release: Release::Full,
entry_points: EntryPoints {
listener: false,
..EntryPoints::HTTP
},
},
register! {
capability: impls::InboundWebhooks,
name: "inbound_webhooks",
feature: "inbound",
compiled_sections: &[],
runtime_keys: &["webhooks", "inbound_dispatch"],
docs: Some("docs/architecture/webhooks.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::InboundEmail,
name: "inbound_email",
feature: "inbound-email",
compiled_sections: &[],
runtime_keys: &["mailbox", "send"],
docs: Some("docs/architecture/inbound-email.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::Identity,
name: "identity",
feature: "auth",
compiled_sections: &[],
runtime_keys: &["identity"],
docs: Some("docs/auth/identity-store.md"),
maturity: Maturity::Stable,
release: Release::Default,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::SessionState,
name: "session_state",
feature: "auth",
compiled_sections: &[],
runtime_keys: &["session_state"],
docs: Some("docs/features/session-state.md"),
maturity: Maturity::Stable,
release: Release::Default,
entry_points: EntryPoints::ALL,
},
register! {
capability: impls::Saml,
name: "saml",
feature: "auth-saml",
compiled_sections: &[],
runtime_keys: &["saml"],
docs: Some("docs/auth/saml-sso.md"),
maturity: Maturity::Preview,
release: Release::Source,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::CdcOutbound,
name: "cdc_outbound",
feature: "cdc-outbound",
compiled_sections: &[],
runtime_keys: &["cdc_outbound"],
docs: Some("docs/features/cdc-outbound.md"),
maturity: Maturity::Stable,
release: Release::Platform,
entry_points: EntryPoints::HTTP,
},
register! {
capability: impls::SubscriptionKafka,
name: "subscription_kafka",
feature: "subscription-kafka",
compiled_sections: &[],
runtime_keys: &["subscription_kafka"],
docs: Some("docs/features/cdc-outbound.md"),
maturity: Maturity::Preview,
release: Release::Source,
entry_points: EntryPoints::HTTP,
},
];
#[must_use]
pub fn feature_gated_keys() -> Vec<(&'static str, &'static str, bool)> {
REGISTRY
.iter()
.flat_map(|r| r.meta.runtime_keys.iter().map(|key| (*key, r.meta.feature, r.compiled_in)))
.collect()
}
#[cfg(test)]
mod manifest_tests;
#[derive(Debug, Default)]
pub struct CapabilityStarts(RwLock<Recorded>);
#[derive(Debug, Default)]
struct Recorded {
started: BTreeMap<&'static str, bool>,
serving: BTreeSet<&'static str>,
}
impl CapabilityStarts {
fn write(&self) -> std::sync::RwLockWriteGuard<'_, Recorded> {
self.0.write().unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn record(&self, name: &'static str, started: bool) {
self.write().started.insert(name, started);
}
#[cfg(all(test, feature = "observers"))]
pub(crate) fn started(&self, name: &str) -> Option<bool> {
self.0
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.started
.get(name)
.copied()
}
pub(crate) fn record_serving(&self, name: &'static str) {
self.write().serving.insert(name);
}
pub async fn health(&self, app: &AppState) -> BTreeMap<&'static str, CapabilityHealth> {
let (started, serving) = {
let recorded = self.0.read().unwrap_or_else(std::sync::PoisonError::into_inner);
(recorded.started.clone(), recorded.serving.clone())
};
let mut report = BTreeMap::new();
for entry in REGISTRY {
let healthy = if entry.compiled_in {
entry.capability.health(app).await
} else {
None
};
report.insert(
entry.meta.name,
CapabilityHealth {
compiled_in: entry.compiled_in,
started: started.get(entry.meta.name).copied(),
serving: serving.contains(entry.meta.name),
healthy,
},
);
}
report
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct CapabilityHealth {
pub compiled_in: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub started: Option<bool>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub serving: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub healthy: Option<bool>,
}