use crate::observation::ObservationSchema;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ContractId(Uuid);
impl ContractId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for ContractId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ContractId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ContractVersion {
major: u32,
minor: u32,
patch: u32,
}
impl ContractVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn bump_major(&self) -> Self {
Self {
major: self.major + 1,
minor: 0,
patch: 0,
}
}
pub fn bump_minor(&self) -> Self {
Self {
major: self.major,
minor: self.minor + 1,
patch: 0,
}
}
pub fn bump_patch(&self) -> Self {
Self {
major: self.major,
minor: self.minor,
patch: self.patch + 1,
}
}
pub fn compatible_with(&self, other: &Self) -> bool {
self.major == other.major && self >= other
}
}
impl fmt::Display for ContractVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl Default for ContractVersion {
fn default() -> Self {
Self::new(1, 0, 0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
id: ContractId,
version: ContractVersion,
name: String,
description: String,
observation_schemas: BTreeMap<String, ObservationSchema>,
decision_patterns: BTreeMap<String, DecisionPattern>,
invariants: Vec<InvariantConstraint>,
published_at: DateTime<Utc>,
signature: Option<String>,
stability: StabilityLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StabilityLevel {
Experimental,
Beta,
Stable,
}
impl fmt::Display for StabilityLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StabilityLevel::Experimental => write!(f, "experimental"),
StabilityLevel::Beta => write!(f, "beta"),
StabilityLevel::Stable => write!(f, "stable"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionPattern {
name: String,
preconditions: Vec<String>,
postconditions: Vec<String>,
idempotent: bool,
}
impl DecisionPattern {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
preconditions: Vec::new(),
postconditions: Vec::new(),
idempotent: false,
}
}
pub fn with_precondition(mut self, condition: impl Into<String>) -> Self {
self.preconditions.push(condition.into());
self
}
pub fn with_postcondition(mut self, condition: impl Into<String>) -> Self {
self.postconditions.push(condition.into());
self
}
pub fn idempotent(mut self) -> Self {
self.idempotent = true;
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_idempotent(&self) -> bool {
self.idempotent
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvariantConstraint {
name: String,
constraint: String,
blocking: bool,
severity: ConstraintSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConstraintSeverity {
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for ConstraintSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConstraintSeverity::Info => write!(f, "info"),
ConstraintSeverity::Warning => write!(f, "warning"),
ConstraintSeverity::Error => write!(f, "error"),
ConstraintSeverity::Critical => write!(f, "critical"),
}
}
}
impl Contract {
pub fn new(
name: impl Into<String>, description: impl Into<String>, stability: StabilityLevel,
) -> Self {
Self {
id: ContractId::new(),
version: ContractVersion::default(),
name: name.into(),
description: description.into(),
observation_schemas: BTreeMap::new(),
decision_patterns: BTreeMap::new(),
invariants: Vec::new(),
published_at: Utc::now(),
signature: None,
stability,
}
}
pub fn id(&self) -> ContractId {
self.id
}
pub fn version(&self) -> &ContractVersion {
&self.version
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn with_observation_schema(
mut self, name: impl Into<String>, schema: ObservationSchema,
) -> Self {
self.observation_schemas.insert(name.into(), schema);
self
}
pub fn with_decision_pattern(mut self, pattern: DecisionPattern) -> Self {
self.decision_patterns
.insert(pattern.name().to_string(), pattern);
self
}
pub fn with_invariant(
mut self, name: impl Into<String>, constraint: impl Into<String>,
severity: ConstraintSeverity, blocking: bool,
) -> Self {
self.invariants.push(InvariantConstraint {
name: name.into(),
constraint: constraint.into(),
blocking,
severity,
});
self
}
pub fn observation_schemas(&self) -> &BTreeMap<String, ObservationSchema> {
&self.observation_schemas
}
pub fn observation_schema(&self, name: &str) -> Option<&ObservationSchema> {
self.observation_schemas.get(name)
}
pub fn decision_patterns(&self) -> &BTreeMap<String, DecisionPattern> {
&self.decision_patterns
}
pub fn decision_pattern(&self, name: &str) -> Option<&DecisionPattern> {
self.decision_patterns.get(name)
}
pub fn invariants(&self) -> &[InvariantConstraint] {
&self.invariants
}
pub fn blocking_invariants(&self) -> Vec<&InvariantConstraint> {
self.invariants.iter().filter(|i| i.blocking).collect()
}
pub fn sign(mut self, key: &[u8]) -> Self {
use hmac::Mac;
let mut mac =
hmac::Hmac::<sha2::Sha256>::new_from_slice(key).expect("HMAC key length is valid");
let payload = format!("{}{}{}", self.id, self.version, self.name);
mac.update(payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
self.signature = Some(signature);
self
}
pub fn stability(&self) -> StabilityLevel {
self.stability
}
pub fn is_stable(&self) -> bool {
self.stability == StabilityLevel::Stable
}
}
impl fmt::Display for Contract {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Contract(id={}, name={}, version={}, stability={})",
self.id, self.name, self.version, self.stability
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ontology {
id: String,
contracts: BTreeMap<ContractId, Contract>,
version: ContractVersion,
created_at: DateTime<Utc>,
}
impl Ontology {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
contracts: BTreeMap::new(),
version: ContractVersion::default(),
created_at: Utc::now(),
}
}
pub fn with_contract(mut self, contract: Contract) -> Self {
self.contracts.insert(contract.id(), contract);
self
}
pub fn contracts(&self) -> &BTreeMap<ContractId, Contract> {
&self.contracts
}
pub fn contract(&self, id: ContractId) -> Option<&Contract> {
self.contracts.get(&id)
}
pub fn find_by_name(&self, name: &str) -> Vec<&Contract> {
self.contracts
.values()
.filter(|c| c.name() == name)
.collect()
}
pub fn id(&self) -> &str {
&self.id
}
pub fn version(&self) -> &ContractVersion {
&self.version
}
pub fn bump_version(&mut self) {
self.version = self.version.bump_minor();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contract_version_compatibility() {
let v1 = ContractVersion::new(1, 0, 0);
let v1_1 = ContractVersion::new(1, 1, 0);
let v2 = ContractVersion::new(2, 0, 0);
assert!(v1_1.compatible_with(&v1));
assert!(!v2.compatible_with(&v1));
}
#[test]
fn test_contract_creation() {
let contract = Contract::new("test", "test contract", StabilityLevel::Beta);
assert_eq!(contract.name(), "test");
assert_eq!(contract.stability(), StabilityLevel::Beta);
}
#[test]
fn test_decision_pattern_idempotence() {
let pattern = DecisionPattern::new("test").idempotent();
assert!(pattern.is_idempotent());
let non_idempotent = DecisionPattern::new("test2");
assert!(!non_idempotent.is_idempotent());
}
#[test]
fn test_ontology_operations() {
let contract1 = Contract::new("c1", "desc1", StabilityLevel::Stable);
let contract2 = Contract::new("c2", "desc2", StabilityLevel::Beta);
let ontology = Ontology::new("test-ontology")
.with_contract(contract1.clone())
.with_contract(contract2.clone());
assert_eq!(ontology.contracts().len(), 2);
assert!(ontology.find_by_name("c1").len() > 0);
}
}