#![allow(missing_docs)]
use crate::crossing::CrossingRecord;
use crate::quad::Tree;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SchemaFieldType {
String,
Number,
Boolean,
Object,
Array,
}
impl SchemaFieldType {
pub fn as_str(&self) -> &'static str {
match self {
Self::String => "string",
Self::Number => "number",
Self::Boolean => "boolean",
Self::Object => "object",
Self::Array => "array",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"string" => Some(Self::String),
"number" => Some(Self::Number),
"boolean" => Some(Self::Boolean),
"object" => Some(Self::Object),
"array" => Some(Self::Array),
_ => None,
}
}
}
impl std::fmt::Display for SchemaFieldType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct SchemaField {
pub name: String,
pub field_type: SchemaFieldType,
pub required: bool,
pub validation: Option<String>,
}
impl SchemaField {
pub fn required(name: impl Into<String>, field_type: SchemaFieldType) -> Self {
Self {
name: name.into(),
field_type,
required: true,
validation: None,
}
}
pub fn optional(name: impl Into<String>, field_type: SchemaFieldType) -> Self {
Self {
name: name.into(),
field_type,
required: false,
validation: None,
}
}
pub fn with_validation(mut self, expression: impl Into<String>) -> Self {
self.validation = Some(expression.into());
self
}
}
#[derive(Debug, Clone)]
pub struct CommandSchema {
pub command_type: String,
pub fields: Vec<SchemaField>,
}
impl CommandSchema {
pub fn new(command_type: impl Into<String>) -> Self {
Self {
command_type: command_type.into(),
fields: Vec::new(),
}
}
pub fn field(mut self, field: SchemaField) -> Self {
self.fields.push(field);
self
}
}
#[deprecated(
since = "0.2.0",
note = "Phase is encoded in the trait type; RingRelationship is decorative. \
Remove fn relationship() from Extension impls."
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RingRelationship {
Before,
Through,
After,
Around,
Within,
}
#[allow(deprecated)]
impl std::fmt::Display for RingRelationship {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Before => write!(f, "BEFORE"),
Self::Through => write!(f, "THROUGH"),
Self::After => write!(f, "AFTER"),
Self::Around => write!(f, "AROUND"),
Self::Within => write!(f, "WITHIN"),
}
}
}
pub trait Extension: Send + Sync {
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct GateRejection {
pub source: String,
pub reason: String,
}
impl std::fmt::Display for GateRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.source, self.reason)
}
}
impl std::error::Error for GateRejection {}
pub trait BeforeRing: Extension {
fn evaluate(&self, context: &Tree) -> Result<(), GateRejection>;
fn feedback(&self, _cycle_index: u64, _output: &Tree) {}
}
#[derive(Debug, Clone)]
pub struct DelegationError {
pub source: String,
pub reason: String,
}
impl std::fmt::Display for DelegationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.source, self.reason)
}
}
impl std::error::Error for DelegationError {}
pub trait ThroughRing: Extension {
fn handles(&self, command_type: &str) -> bool;
fn dispatch(&self, input: &Tree) -> Result<Tree, DelegationError>;
fn permission_requirements(&self) -> Vec<PermissionRequirement> {
Vec::new()
}
fn claimed_prefixes(&self) -> Vec<String> {
Vec::new()
}
fn command_schemas(&self) -> Vec<CommandSchema> {
Vec::new()
}
}
pub use crate::evidence::{
verify_ring_cycle_evidence, verify_ring_cycle_evidence_with_key, verify_view_cycle_evidence,
};
pub trait ViewRing: Extension {
fn handles_view(&self, view_id: &str) -> bool;
fn project(&self, input: &Tree) -> Result<Tree, DelegationError>;
fn claimed_view_prefixes(&self) -> Vec<String> {
Vec::new()
}
}
pub trait AfterRing: Extension {
fn on_cycle_complete(&self, cycle_index: u64, output: &Tree);
fn on_crossings(&self, _cycle_index: u64, _records: &[CrossingRecord]) {}
}
pub trait AroundRing: Extension {
fn inject(&self, fu_data: &mut Tree);
fn observe(&self, cycle_index: u64, ou_output: &Tree, su_output: &Tree);
}
pub use crate::sibling::{
CapabilityDeclaration, DomainRequest, HttpMethod, RegistrationError, RouteMount,
SiblingManifest, SiblingStatus, SurfaceSection,
};
pub use crate::authorization::{
AuthorizationTable, AuthorizationTableEntry, AuthorizationTableResult, PermissionCollision,
PermissionRegistry, PermissionRequirement, SharedAuthorizationTable, SharedPermissionRegistry,
};
#[cfg(test)]
mod tests {
use super::*;
use crate::evidence::{verify_ring_cycle_evidence, verify_ring_cycle_evidence_with_key};
use std::sync::Arc;
#[test]
fn extension_is_object_safe() {
fn _assert(_: &dyn Extension) {}
}
#[test]
fn before_ring_is_object_safe() {
fn _assert(_: &dyn BeforeRing) {}
}
#[test]
fn through_ring_is_object_safe() {
fn _assert(_: &dyn ThroughRing) {}
}
#[test]
fn after_ring_is_object_safe() {
fn _assert(_: &dyn AfterRing) {}
}
#[test]
fn around_ring_is_object_safe() {
fn _assert(_: &dyn AroundRing) {}
}
#[allow(deprecated)]
#[test]
fn relationship_display() {
assert_eq!(RingRelationship::Before.to_string(), "BEFORE");
assert_eq!(RingRelationship::Through.to_string(), "THROUGH");
assert_eq!(RingRelationship::After.to_string(), "AFTER");
assert_eq!(RingRelationship::Around.to_string(), "AROUND");
assert_eq!(RingRelationship::Within.to_string(), "WITHIN");
}
#[allow(deprecated)]
#[test]
fn relationship_equality() {
assert_eq!(RingRelationship::Before, RingRelationship::Before);
assert_ne!(RingRelationship::Before, RingRelationship::After);
}
#[test]
fn gate_rejection_display() {
let r = GateRejection {
source: "gate-a".into(),
reason: "rate limit".into(),
};
assert_eq!(r.to_string(), "gate-a: rate limit");
}
#[test]
fn delegation_error_display() {
let e = DelegationError {
source: "delegate-b".into(),
reason: "not found".into(),
};
assert_eq!(e.to_string(), "delegate-b: not found");
}
#[test]
fn sibling_manifest_is_object_safe() {
fn _assert(_: &dyn SiblingManifest) {}
}
#[test]
fn registration_error_display() {
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-v1".into(),
};
assert!(e.to_string().contains("already claimed by"));
let e = RegistrationError::IncompatibleVersion {
required: "2.0.0".into(),
running: "1.0.0".into(),
};
assert!(e.to_string().contains("requires ring"));
let e = RegistrationError::ProvisionFailed("disk full".into());
assert!(e.to_string().contains("disk full"));
}
#[test]
fn ring_cycle_evidence_present() {
let mut tree = Tree::new();
tree.insert("command.request_id".into(), b"req-abc-123".to_vec());
tree.insert("command.type".into(), b"sibling.qi.analyze".to_vec());
assert!(verify_ring_cycle_evidence("qi", &tree).is_ok());
}
#[test]
fn ring_cycle_evidence_missing_rejects() {
let mut tree = Tree::new();
tree.insert("command.type".into(), b"sibling.qi.analyze".to_vec());
let err = verify_ring_cycle_evidence("qi", &tree).unwrap_err();
assert!(err.to_string().contains("outside ring cycle"));
}
#[test]
fn ring_cycle_evidence_empty_tree_rejects() {
let tree = Tree::new();
assert!(verify_ring_cycle_evidence("test", &tree).is_err());
}
#[test]
fn ring_cycle_evidence_with_key_custom_marker() {
let mut tree = Tree::new();
tree.insert("command.operator".into(), b"alice".to_vec());
tree.insert("command.type".into(), b"tenant.provision".to_vec());
assert!(
verify_ring_cycle_evidence_with_key("tenant-through", &tree, "command.operator")
.is_ok()
);
}
#[test]
fn ring_cycle_evidence_with_key_missing_custom_marker_rejects() {
let mut tree = Tree::new();
tree.insert("command.type".into(), b"tenant.provision".to_vec());
let err =
verify_ring_cycle_evidence_with_key("tenant-through", &tree, "command.operator")
.unwrap_err();
assert!(err.to_string().contains("command.operator"));
}
struct PermDeclThrough {
perms: Vec<PermissionRequirement>,
}
impl Extension for PermDeclThrough {
fn name(&self) -> &str {
"perm-test"
}
}
impl ThroughRing for PermDeclThrough {
fn handles(&self, cmd: &str) -> bool {
cmd.starts_with("sibling.test.")
}
fn dispatch(&self, _input: &Tree) -> Result<Tree, DelegationError> {
Ok(Tree::new())
}
fn permission_requirements(&self) -> Vec<PermissionRequirement> {
self.perms.clone()
}
}
struct DelegatingManifest {
through: Option<Arc<dyn ThroughRing>>,
}
impl SiblingManifest for DelegatingManifest {
fn id(&self) -> &str {
"test"
}
fn display_name(&self) -> &str {
"Test"
}
fn version(&self) -> &str {
"1.0.0"
}
fn min_ring_version(&self) -> &str {
"0.1.0"
}
fn through_handler(&self) -> Option<Arc<dyn ThroughRing>> {
self.through.clone()
}
}
#[test]
fn manifest_permission_requirements_delegates_to_through_handler() {
let through = Arc::new(PermDeclThrough {
perms: vec![PermissionRequirement::new(
"sibling.test.analyze",
"operator",
"a3",
"routine",
"a3",
)],
});
let manifest = DelegatingManifest {
through: Some(through),
};
let reqs = manifest.permission_requirements();
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].command, "sibling.test.analyze");
assert_eq!(reqs[0].min_role_key, "operator");
}
#[test]
fn manifest_permission_requirements_empty_without_through_handler() {
let manifest = DelegatingManifest { through: None };
assert!(manifest.permission_requirements().is_empty());
}
#[test]
fn manifest_permission_requirements_empty_when_handler_declares_none() {
let through = Arc::new(PermDeclThrough { perms: vec![] });
let manifest = DelegatingManifest {
through: Some(through),
};
assert!(manifest.permission_requirements().is_empty());
}
#[allow(deprecated)]
#[test]
fn five_relationships_exhaustive() {
let all = [
RingRelationship::Before,
RingRelationship::Through,
RingRelationship::After,
RingRelationship::Around,
RingRelationship::Within,
];
assert_eq!(all.len(), 5);
for (i, a) in all.iter().enumerate() {
for (j, b) in all.iter().enumerate() {
if i != j {
assert_ne!(a, b);
}
}
}
}
#[test]
fn schema_field_type_display_and_parse_roundtrip() {
let types = [
SchemaFieldType::String,
SchemaFieldType::Number,
SchemaFieldType::Boolean,
SchemaFieldType::Object,
SchemaFieldType::Array,
];
for ft in types {
let s = ft.as_str();
let parsed = SchemaFieldType::parse(s).unwrap();
assert_eq!(parsed, ft);
assert_eq!(ft.to_string(), s);
}
}
#[test]
fn schema_field_type_parse_unknown_returns_none() {
assert!(SchemaFieldType::parse("unknown").is_none());
assert!(SchemaFieldType::parse("").is_none());
}
#[test]
fn schema_field_required_and_optional() {
let req = SchemaField::required("username", SchemaFieldType::String);
assert!(req.required);
assert_eq!(req.name, "username");
assert_eq!(req.field_type, SchemaFieldType::String);
let opt = SchemaField::optional("timeout", SchemaFieldType::Number);
assert!(!opt.required);
assert_eq!(opt.name, "timeout");
}
#[test]
fn command_schema_builder() {
let schema = CommandSchema::new("user.create")
.field(SchemaField::required("username", SchemaFieldType::String))
.field(SchemaField::required("password", SchemaFieldType::String))
.field(SchemaField::optional("role", SchemaFieldType::String));
assert_eq!(schema.command_type, "user.create");
assert_eq!(schema.fields.len(), 3);
assert!(schema.fields[0].required);
assert!(schema.fields[1].required);
assert!(!schema.fields[2].required);
}
#[test]
fn command_schema_empty_fields() {
let schema = CommandSchema::new("user.list");
assert_eq!(schema.command_type, "user.list");
assert!(schema.fields.is_empty());
}
#[test]
fn through_ring_command_schemas_default_empty() {
let through = PermDeclThrough { perms: vec![] };
assert!(through.command_schemas().is_empty());
}
#[test]
fn schema_field_with_validation() {
let field = SchemaField::required("timeout", SchemaFieldType::Number)
.with_validation("timeout <= 30000");
assert!(field.required);
assert_eq!(field.validation.as_deref(), Some("timeout <= 30000"));
let plain = SchemaField::optional("debug", SchemaFieldType::Boolean);
assert!(plain.validation.is_none());
}
}