use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct OkResponse {
pub ok: bool,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct OkIdResponse {
pub ok: bool,
pub id: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct IdResponse {
pub id: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct CreatedResponse {
pub created: Vec<String>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct ErrorResponse {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct StatusResponse {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub plugin_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
#[serde(rename_all = "lowercase")]
pub enum ServiceStatus {
Ok,
Degraded,
Unhealthy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
Dev,
Nightly,
Prod,
Mock,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct HealthResponse {
pub status: ServiceStatus,
pub version: String,
pub kind: BackendKind,
pub uptime: u64,
pub network: NetworkInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub engine_version: Option<String>,
}
impl HealthResponse {
pub fn ok(
version: impl Into<String>,
kind: BackendKind,
uptime: u64,
network: NetworkInfo,
) -> Self {
Self {
status: ServiceStatus::Ok,
version: version.into(),
kind,
uptime,
network,
build_hash: None,
engine_version: None,
}
}
pub fn with_build(
mut self,
build_hash: impl Into<String>,
engine_version: Option<String>,
) -> Self {
let hash = build_hash.into();
if !hash.is_empty() {
self.build_hash = Some(hash);
}
self.engine_version = engine_version;
self
}
pub fn version_report(&self) -> VersionReport {
VersionReport {
version: self.version.clone(),
build_hash: self.build_hash.clone(),
kind: self.kind,
engine_version: self.engine_version.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct VersionReport {
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub build_hash: Option<String>,
pub kind: BackendKind,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub engine_version: Option<String>,
}
pub const SERVICE_INFO_METHOD: &str = "Service.Info";
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct NetworkInfo {
pub transport: String,
pub region: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub asn: Option<u32>,
}
impl NetworkInfo {
pub fn unknown() -> Self {
Self {
transport: "sse".into(),
region: "XX".into(),
asn: None,
}
}
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct ConnectionStatus {
pub connected: bool,
pub latency: u64,
#[serde(rename = "lastCheck")]
pub last_check: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct RbacUser {
pub id: String,
pub username: String,
pub email: Option<String>,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub is_active: bool,
pub role: String,
pub tier: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct RbacUsersResponse {
pub users: Vec<RbacUser>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct RbacGroup {
pub id: String,
pub name: String,
pub description: Option<String>,
pub member_count: u32,
pub enforce_personal_visibility: bool,
pub is_builtin: bool,
pub builtin_key: Option<String>,
pub display_names: Option<std::collections::BTreeMap<String, String>>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct RbacGroupsResponse {
pub groups: Vec<RbacGroup>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct MyPermissions {
pub role: String,
pub permissions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct PermissionsResponse {
pub role: String,
pub permissions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct GrantItem {
pub id: String,
pub scope: String,
pub user_id: Option<String>,
pub group_id: Option<String>,
pub permission: String,
pub resource_id: Option<String>,
pub granted: bool,
pub created_at: String,
}
impl GrantItem {
#[must_use]
pub fn validate_permission(&self) -> Option<&str> {
let valid = crate::protocol_core::rbac::Permission::from_path(&self.permission).is_some()
|| !crate::protocol_core::rbac::Permission::expand_domain(&self.permission).is_empty();
if valid {
None
} else {
Some(&self.permission)
}
}
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct GrantListResponse {
pub grants: Vec<GrantItem>,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct OAuthProvider {
pub provider: String,
pub client_id: String,
pub client_secret_masked: String,
pub public_domain: String,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct DeletedResponse {
pub deleted: u64,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct OkMessageResponse {
pub ok: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "httpTypes.ts")]
pub struct ReadinessResponse {
pub status: String,
pub database: bool,
}
#[cfg(test)]
mod health_serde_tests {
use super::*;
#[test]
fn optional_fields_stay_absent_when_none() {
let health = HealthResponse::ok(
"0.1.0 master:abc1234",
BackendKind::Prod,
60,
NetworkInfo::unknown(),
);
let json = serde_json::to_value(&health).unwrap();
let object = json.as_object().unwrap();
assert!(
!object.contains_key("build_hash"),
"None -> absent: {object:?}"
);
assert!(!object.contains_key("engine_version"), "None -> absent");
assert_eq!(object.get("status").and_then(|v| v.as_str()), Some("ok"));
assert_eq!(object.get("kind").and_then(|v| v.as_str()), Some("prod"));
}
#[test]
fn optional_fields_serialize_when_set() {
let mut health = HealthResponse::ok("v", BackendKind::Dev, 1, NetworkInfo::unknown());
health.build_hash = Some("abc1234".into());
health.engine_version = Some("v".into());
let json = serde_json::to_value(&health).unwrap();
assert_eq!(json["build_hash"], "abc1234");
assert_eq!(json["engine_version"], "v");
}
}