#![allow(missing_docs)]
use std::sync::Arc;
use crate::extension::{AfterRing, AroundRing, BeforeRing, ThroughRing};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SiblingStatus {
Validated,
Provisioned,
Registered,
Active,
Failed,
}
impl std::fmt::Display for SiblingStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Validated => write!(f, "validated"),
Self::Provisioned => write!(f, "provisioned"),
Self::Registered => write!(f, "registered"),
Self::Active => write!(f, "active"),
Self::Failed => write!(f, "failed"),
}
}
}
#[derive(Debug, Clone)]
pub struct DomainRequest {
pub sibling_id: String,
pub display_name: String,
pub version: String,
pub max_capacity_bytes: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct CapabilityDeclaration {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone)]
pub struct SurfaceSection {
pub id: String,
pub label: String,
pub icon: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
}
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Delete => write!(f, "DELETE"),
}
}
}
#[derive(Debug, Clone)]
pub struct RouteMount {
pub path: String,
pub method: HttpMethod,
pub description: String,
}
pub trait SiblingManifest: Send + Sync {
fn id(&self) -> &str;
fn display_name(&self) -> &str;
fn version(&self) -> &str;
fn min_ring_version(&self) -> &str;
fn through_handler(&self) -> Option<Arc<dyn ThroughRing>>;
fn before_gate(&self) -> Option<Box<dyn BeforeRing>> {
None
}
fn after_observer(&self) -> Option<Box<dyn AfterRing>> {
None
}
fn around_extension(&self) -> Option<Box<dyn AroundRing>> {
None
}
fn persistence_domains(&self) -> Vec<DomainRequest> {
vec![]
}
fn capabilities(&self) -> Vec<CapabilityDeclaration> {
vec![]
}
fn surface_sections(&self) -> Vec<SurfaceSection> {
vec![]
}
fn structural_routes(&self) -> Vec<RouteMount> {
vec![]
}
fn permission_requirements(&self) -> Vec<crate::authorization::PermissionRequirement> {
self.through_handler()
.map(|h| h.permission_requirements())
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RegistrationError {
InvalidId(String),
DuplicateId(String),
NamespaceConflict { prefix: String, claimed_by: String },
RouteCollision(String),
IncompatibleVersion { required: String, running: String },
ProvisionFailed(String),
CoordinateValidation(String),
}
impl std::fmt::Display for RegistrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidId(id) => write!(f, "invalid sibling ID: {id}"),
Self::DuplicateId(id) => write!(f, "sibling '{id}' already registered"),
Self::NamespaceConflict { prefix, claimed_by } => {
write!(f, "namespace '{prefix}' already claimed by '{claimed_by}'")
}
Self::RouteCollision(path) => write!(f, "route collision: {path}"),
Self::IncompatibleVersion { required, running } => {
write!(f, "sibling requires ring {required}, running {running}")
}
Self::ProvisionFailed(reason) => write!(f, "provisioning failed: {reason}"),
Self::CoordinateValidation(msg) => {
write!(f, "lexicon coordinate validation failed: {msg}")
}
}
}
}
impl std::error::Error for RegistrationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sibling_manifest_is_object_safe() {
fn _assert(_: &dyn SiblingManifest) {}
}
#[test]
fn sibling_status_display() {
assert_eq!(SiblingStatus::Validated.to_string(), "validated");
assert_eq!(SiblingStatus::Provisioned.to_string(), "provisioned");
assert_eq!(SiblingStatus::Registered.to_string(), "registered");
assert_eq!(SiblingStatus::Active.to_string(), "active");
assert_eq!(SiblingStatus::Failed.to_string(), "failed");
}
#[test]
fn sibling_status_equality() {
assert_eq!(SiblingStatus::Active, SiblingStatus::Active);
assert_ne!(SiblingStatus::Active, SiblingStatus::Failed);
}
#[test]
fn registration_error_display() {
let e = RegistrationError::InvalidId("bad!id".into());
assert_eq!(e.to_string(), "invalid sibling ID: bad!id");
let e = RegistrationError::DuplicateId("qi".into());
assert_eq!(e.to_string(), "sibling 'qi' already registered");
let e = RegistrationError::NamespaceConflict { prefix: "sibling.qi.*".into(), claimed_by: "qi".into() };
assert_eq!(e.to_string(), "namespace 'sibling.qi.*' already claimed by 'qi'");
let e = RegistrationError::RouteCollision("/oauth/callback".into());
assert_eq!(e.to_string(), "route collision: /oauth/callback");
let e = RegistrationError::IncompatibleVersion {
required: "0.2.0".into(),
running: "0.1.0".into(),
};
assert_eq!(e.to_string(), "sibling requires ring 0.2.0, running 0.1.0");
let e = RegistrationError::ProvisionFailed("disk full".into());
assert_eq!(e.to_string(), "provisioning failed: disk full");
}
#[test]
fn http_method_display() {
assert_eq!(HttpMethod::Get.to_string(), "GET");
assert_eq!(HttpMethod::Post.to_string(), "POST");
assert_eq!(HttpMethod::Put.to_string(), "PUT");
assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
}
#[test]
fn domain_request_creation() {
let req = DomainRequest {
sibling_id: "qi".into(),
display_name: "Quality Intelligence".into(),
version: "0.1.0".into(),
max_capacity_bytes: Some(128 * 1024 * 1024),
};
assert_eq!(req.sibling_id, "qi");
assert_eq!(req.max_capacity_bytes, Some(128 * 1024 * 1024));
}
#[test]
fn capability_declaration_creation() {
let cap = CapabilityDeclaration {
name: "qi.contract.verify".into(),
description: "Verify QI contracts".into(),
};
assert_eq!(cap.name, "qi.contract.verify");
}
#[test]
fn surface_section_creation() {
let section = SurfaceSection {
id: "qi-contracts".into(),
label: "QI Contracts".into(),
icon: Some("shield".into()),
};
assert_eq!(section.id, "qi-contracts");
assert_eq!(section.icon, Some("shield".into()));
}
#[test]
fn route_mount_creation() {
let route = RouteMount {
path: "/sibling/git/webhook".into(),
method: HttpMethod::Post,
description: "Git webhook receiver".into(),
};
assert_eq!(route.path, "/sibling/git/webhook");
assert_eq!(route.method, HttpMethod::Post);
}
}