use std::error::Error;
use std::fmt;
use crate::{BoxId, CapabilityId, CapabilityName, ContractRevision, Deprecation, TypeDescriptor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CapabilityShape {
Unary,
ServerStreaming,
ClientStreaming,
BidirectionalStreaming,
EventSubscription,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ExposureLevel {
CodeOnly,
Internal,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Idempotency {
None,
Inherent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityDescriptor {
id: CapabilityId,
input: TypeDescriptor,
output: TypeDescriptor,
error: TypeDescriptor,
shape: CapabilityShape,
max_exposure: ExposureLevel,
idempotency: Idempotency,
deprecation: Option<Deprecation>,
}
impl CapabilityDescriptor {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: CapabilityId,
input: TypeDescriptor,
output: TypeDescriptor,
error: TypeDescriptor,
shape: CapabilityShape,
max_exposure: ExposureLevel,
idempotency: Idempotency,
deprecation: Option<Deprecation>,
) -> Self {
Self {
id,
input,
output,
error,
shape,
max_exposure,
idempotency,
deprecation,
}
}
pub fn name(&self) -> &CapabilityName {
self.id.name()
}
pub fn id(&self) -> &CapabilityId {
&self.id
}
pub fn input(&self) -> &TypeDescriptor {
&self.input
}
pub fn output(&self) -> &TypeDescriptor {
&self.output
}
pub fn error(&self) -> &TypeDescriptor {
&self.error
}
pub fn shape(&self) -> CapabilityShape {
self.shape
}
pub fn max_exposure(&self) -> ExposureLevel {
self.max_exposure
}
pub fn idempotency(&self) -> Idempotency {
self.idempotency
}
pub fn deprecation(&self) -> Option<&Deprecation> {
self.deprecation.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractDescriptor {
box_id: BoxId,
capabilities: Vec<CapabilityDescriptor>,
revision: ContractRevision,
}
impl ContractDescriptor {
pub fn new(
box_id: BoxId,
capabilities: impl IntoIterator<Item = CapabilityDescriptor>,
revision: ContractRevision,
) -> Result<Self, ContractDescriptorError> {
let mut accepted: Vec<CapabilityDescriptor> = Vec::new();
for capability in capabilities {
if capability.id().box_id() != &box_id {
return Err(ContractDescriptorError::CapabilityBoxMismatch {
contract_box: box_id,
capability: capability.id,
});
}
if accepted.iter().any(|known| known.id() == capability.id()) {
return Err(ContractDescriptorError::DuplicateCapability {
capability: capability.id,
});
}
accepted.push(capability);
}
Ok(Self {
box_id,
capabilities: accepted,
revision,
})
}
pub fn box_id(&self) -> &BoxId {
&self.box_id
}
pub fn capabilities(&self) -> &[CapabilityDescriptor] {
&self.capabilities
}
pub fn revision(&self) -> &ContractRevision {
&self.revision
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContractDescriptorError {
CapabilityBoxMismatch {
contract_box: BoxId,
capability: CapabilityId,
},
DuplicateCapability {
capability: CapabilityId,
},
}
impl fmt::Display for ContractDescriptorError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CapabilityBoxMismatch {
contract_box,
capability,
} => write!(
formatter,
"capability {capability} does not belong to contract box {contract_box}"
),
Self::DuplicateCapability { capability } => {
write!(formatter, "duplicate capability: {capability}")
}
}
}
}
impl Error for ContractDescriptorError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::DescriptorRef;
fn id(box_name: &str, capability: &str) -> CapabilityId {
CapabilityId::new(
BoxId::new(box_name).unwrap(),
CapabilityName::new(capability).unwrap(),
)
}
fn capability(
id: CapabilityId,
shape: CapabilityShape,
idempotency: Idempotency,
) -> CapabilityDescriptor {
CapabilityDescriptor::new(
id,
TypeDescriptor::string(),
TypeDescriptor::u64(),
TypeDescriptor::bool(),
shape,
ExposureLevel::Internal,
idempotency,
(idempotency == Idempotency::None)
.then(|| Deprecation::new(Some("use replacement".into()))),
)
}
#[test]
fn complete_outward_view_preserves_capability_order() {
let box_id = BoxId::new("billing").unwrap();
let first = capability(
id("billing", "quote"),
CapabilityShape::Unary,
Idempotency::None,
);
let second = capability(
id("billing", "invoice"),
CapabilityShape::ServerStreaming,
Idempotency::Inherent,
);
let revision = ContractRevision::new("sha256:123").unwrap();
let contract = ContractDescriptor::new(
box_id.clone(),
[first.clone(), second.clone()],
revision.clone(),
)
.unwrap();
assert_eq!(contract.box_id(), &box_id);
assert_eq!(contract.revision(), &revision);
assert_eq!(contract.capabilities(), &[first, second]);
let viewed = &contract.capabilities()[0];
assert_eq!(viewed.name().as_str(), "quote");
assert_eq!(viewed.id(), &id("billing", "quote"));
assert_eq!(viewed.input().view(), DescriptorRef::String);
assert_eq!(viewed.output().view(), DescriptorRef::U64);
assert_eq!(viewed.error().view(), DescriptorRef::Bool);
assert_eq!(viewed.shape(), CapabilityShape::Unary);
assert_eq!(viewed.max_exposure(), ExposureLevel::Internal);
assert_eq!(viewed.idempotency(), Idempotency::None);
assert_eq!(
viewed.deprecation().unwrap().note(),
Some("use replacement")
);
assert_eq!(contract.capabilities()[1].deprecation(), None);
}
#[test]
fn outward_contract_construction_requires_no_implementation_data() {
let contract = ContractDescriptor::new(
BoxId::new("empty").unwrap(),
[],
ContractRevision::new("r1").unwrap(),
)
.unwrap();
assert!(contract.capabilities().is_empty());
}
#[test]
fn every_interaction_shape_and_idempotency_variant_is_constructible() {
let shapes = [
CapabilityShape::Unary,
CapabilityShape::ServerStreaming,
CapabilityShape::ClientStreaming,
CapabilityShape::BidirectionalStreaming,
CapabilityShape::EventSubscription,
];
for (index, shape) in shapes.into_iter().enumerate() {
let idempotency = if index % 2 == 0 {
Idempotency::None
} else {
Idempotency::Inherent
};
let descriptor = capability(id("box", &format!("cap_{index}")), shape, idempotency);
assert_eq!(descriptor.shape(), shape);
assert_eq!(descriptor.idempotency(), idempotency);
}
}
#[test]
fn exposure_levels_form_the_declared_lattice() {
assert!(ExposureLevel::CodeOnly < ExposureLevel::Internal);
assert!(ExposureLevel::Internal < ExposureLevel::External);
}
#[test]
fn contract_rejects_identity_mismatch_exactly() {
let error = ContractDescriptor::new(
BoxId::new("expected").unwrap(),
[capability(
id("other", "run"),
CapabilityShape::Unary,
Idempotency::None,
)],
ContractRevision::new("r1").unwrap(),
)
.unwrap_err();
assert_eq!(
error,
ContractDescriptorError::CapabilityBoxMismatch {
contract_box: BoxId::new("expected").unwrap(),
capability: id("other", "run"),
}
);
assert_eq!(
error.to_string(),
"capability other.run does not belong to contract box expected"
);
}
#[test]
fn contract_rejects_duplicate_capabilities_exactly() {
let duplicate = capability(id("box", "run"), CapabilityShape::Unary, Idempotency::None);
let error = ContractDescriptor::new(
BoxId::new("box").unwrap(),
[duplicate.clone(), duplicate],
ContractRevision::new("r1").unwrap(),
)
.unwrap_err();
assert_eq!(
error,
ContractDescriptorError::DuplicateCapability {
capability: id("box", "run"),
}
);
assert_eq!(error.to_string(), "duplicate capability: box.run");
}
#[test]
fn descriptors_are_structurally_equal_owned_plain_data() {
fn assert_bounds<T: Send + Sync + 'static>() {}
let build = || {
ContractDescriptor::new(
BoxId::new("box").unwrap(),
[capability(
id("box", "run"),
CapabilityShape::Unary,
Idempotency::None,
)],
ContractRevision::new("r1").unwrap(),
)
.unwrap()
};
assert_eq!(build(), build());
assert_bounds::<CapabilityShape>();
assert_bounds::<ExposureLevel>();
assert_bounds::<Idempotency>();
assert_bounds::<CapabilityDescriptor>();
assert_bounds::<ContractDescriptor>();
assert_bounds::<ContractDescriptorError>();
}
}