use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GrantedLicenseType {
OpenSource,
Commercial,
Research,
Evaluation,
Revoked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseGrant {
pub invention_id: String,
pub licensee_did: String,
pub license_type: GrantedLicenseType,
pub granted_at: u64,
pub expires_at: Option<u64>,
pub attribution_required: bool,
pub royalty_percentage: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LicenseCheckResult {
Allowed,
RequiresLicense { invention_id: String },
Expired {
invention_id: String,
expired_at: u64,
},
Revoked { invention_id: String },
}
#[derive(Debug, Clone, Default)]
pub struct LicenseRegistry {
grants: Vec<LicenseGrant>,
protected_zomes: HashMap<String, Vec<String>>,
}
impl LicenseRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_grant(&mut self, grant: LicenseGrant) {
self.grants.push(grant);
}
pub fn revoke_grant(&mut self, invention_id: &str, licensee_did: &str) -> usize {
let mut count = 0;
for grant in &mut self.grants {
if grant.invention_id == invention_id
&& grant.licensee_did == licensee_did
&& grant.license_type != GrantedLicenseType::Revoked
{
grant.license_type = GrantedLicenseType::Revoked;
count += 1;
}
}
count
}
pub fn protect_zome(&mut self, zome_name: &str, invention_ids: Vec<String>) {
self.protected_zomes
.entry(zome_name.to_string())
.or_default()
.extend(invention_ids);
}
pub fn check_license(
&self,
licensee_did: &str,
target_zome: &str,
now: u64,
) -> LicenseCheckResult {
let required = match self.protected_zomes.get(target_zome) {
Some(ids) if !ids.is_empty() => ids,
_ => return LicenseCheckResult::Allowed,
};
for invention_id in required {
if self.is_open_source(invention_id) {
continue;
}
let grant = self
.grants
.iter()
.filter(|g| g.invention_id == *invention_id && g.licensee_did == licensee_did)
.last();
match grant {
None => {
return LicenseCheckResult::RequiresLicense {
invention_id: invention_id.clone(),
};
}
Some(g) if g.license_type == GrantedLicenseType::Revoked => {
return LicenseCheckResult::Revoked {
invention_id: invention_id.clone(),
};
}
Some(g) => {
if let Some(expires_at) = g.expires_at {
if now >= expires_at {
return LicenseCheckResult::Expired {
invention_id: invention_id.clone(),
expired_at: expires_at,
};
}
}
}
}
}
LicenseCheckResult::Allowed
}
pub fn is_open_source(&self, invention_id: &str) -> bool {
self.grants.iter().any(|g| {
g.invention_id == invention_id && g.license_type == GrantedLicenseType::OpenSource
})
}
pub fn get_grants_for_agent(&self, did: &str) -> Vec<&LicenseGrant> {
self.grants
.iter()
.filter(|g| g.licensee_did == did)
.collect()
}
pub fn expired_grants(&self, now: u64) -> Vec<&LicenseGrant> {
self.grants
.iter()
.filter(|g| {
if let Some(exp) = g.expires_at {
now >= exp
} else {
false
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> u64 {
1_000_000_000_000 }
fn make_grant(
invention_id: &str,
licensee_did: &str,
license_type: GrantedLicenseType,
expires_at: Option<u64>,
) -> LicenseGrant {
LicenseGrant {
invention_id: invention_id.to_string(),
licensee_did: licensee_did.to_string(),
license_type,
granted_at: now(),
expires_at,
attribution_required: false,
royalty_percentage: None,
}
}
#[test]
fn empty_registry() {
let reg = LicenseRegistry::new();
assert!(reg.grants.is_empty());
assert!(reg.protected_zomes.is_empty());
}
#[test]
fn register_and_list_grants() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
reg.register_grant(make_grant(
"inv-002",
"did:mycelix:alice",
GrantedLicenseType::Research,
None,
));
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:bob",
GrantedLicenseType::Evaluation,
Some(now() + 1_000_000),
));
let alice_grants = reg.get_grants_for_agent("did:mycelix:alice");
assert_eq!(alice_grants.len(), 2);
let bob_grants = reg.get_grants_for_agent("did:mycelix:bob");
assert_eq!(bob_grants.len(), 1);
let unknown = reg.get_grants_for_agent("did:mycelix:charlie");
assert!(unknown.is_empty());
}
#[test]
fn unprotected_zome_always_allowed() {
let reg = LicenseRegistry::new();
let result = reg.check_license("did:mycelix:anyone", "some_zome", now());
assert_eq!(result, LicenseCheckResult::Allowed);
}
#[test]
fn open_source_invention_passes() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-oss",
"did:mycelix:community",
GrantedLicenseType::OpenSource,
None,
));
reg.protect_zome("cool_zome", vec!["inv-oss".into()]);
let result = reg.check_license("did:mycelix:random", "cool_zome", now());
assert_eq!(result, LicenseCheckResult::Allowed);
assert!(reg.is_open_source("inv-oss"));
}
#[test]
fn valid_commercial_grant_allows() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None, ));
reg.protect_zome("patented_zome", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:alice", "patented_zome", now());
assert_eq!(result, LicenseCheckResult::Allowed);
}
#[test]
fn no_grant_requires_license() {
let mut reg = LicenseRegistry::new();
reg.protect_zome("patented_zome", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:alice", "patented_zome", now());
assert_eq!(
result,
LicenseCheckResult::RequiresLicense {
invention_id: "inv-001".into()
}
);
}
#[test]
fn expired_grant_returns_expired() {
let mut reg = LicenseRegistry::new();
let expiry = now() + 500_000;
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Evaluation,
Some(expiry),
));
reg.protect_zome("trial_zome", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:alice", "trial_zome", now());
assert_eq!(result, LicenseCheckResult::Allowed);
let result = reg.check_license("did:mycelix:alice", "trial_zome", expiry);
assert_eq!(
result,
LicenseCheckResult::Expired {
invention_id: "inv-001".into(),
expired_at: expiry,
}
);
let result = reg.check_license("did:mycelix:alice", "trial_zome", expiry + 1);
assert_eq!(
result,
LicenseCheckResult::Expired {
invention_id: "inv-001".into(),
expired_at: expiry,
}
);
}
#[test]
fn revoked_grant_returns_revoked() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
reg.protect_zome("patented_zome", vec!["inv-001".into()]);
assert_eq!(
reg.check_license("did:mycelix:alice", "patented_zome", now()),
LicenseCheckResult::Allowed,
);
let count = reg.revoke_grant("inv-001", "did:mycelix:alice");
assert_eq!(count, 1);
assert_eq!(
reg.check_license("did:mycelix:alice", "patented_zome", now()),
LicenseCheckResult::Revoked {
invention_id: "inv-001".into()
},
);
}
#[test]
fn revoke_idempotent() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
assert_eq!(reg.revoke_grant("inv-001", "did:mycelix:alice"), 1);
assert_eq!(reg.revoke_grant("inv-001", "did:mycelix:alice"), 0);
}
#[test]
fn multiple_inventions_all_must_be_licensed() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
reg.protect_zome("multi_zome", vec!["inv-001".into(), "inv-002".into()]);
let result = reg.check_license("did:mycelix:alice", "multi_zome", now());
assert_eq!(
result,
LicenseCheckResult::RequiresLicense {
invention_id: "inv-002".into()
}
);
reg.register_grant(make_grant(
"inv-002",
"did:mycelix:alice",
GrantedLicenseType::Research,
None,
));
let result = reg.check_license("did:mycelix:alice", "multi_zome", now());
assert_eq!(result, LicenseCheckResult::Allowed);
}
#[test]
fn latest_grant_wins() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Evaluation,
Some(now() + 100),
));
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
reg.protect_zome("z", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:alice", "z", now() + 200);
assert_eq!(result, LicenseCheckResult::Allowed);
}
#[test]
fn unknown_agent_requires_license() {
let mut reg = LicenseRegistry::new();
reg.protect_zome("z", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:unknown", "z", now());
assert_eq!(
result,
LicenseCheckResult::RequiresLicense {
invention_id: "inv-001".into()
}
);
}
#[test]
fn unknown_zome_always_allowed() {
let mut reg = LicenseRegistry::new();
reg.protect_zome("other", vec!["inv-001".into()]);
let result = reg.check_license("did:mycelix:alice", "nonexistent", now());
assert_eq!(result, LicenseCheckResult::Allowed);
}
#[test]
fn expired_grants_helper() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Evaluation,
Some(now() + 100),
));
reg.register_grant(make_grant(
"inv-002",
"did:mycelix:bob",
GrantedLicenseType::Commercial,
None, ));
reg.register_grant(make_grant(
"inv-003",
"did:mycelix:charlie",
GrantedLicenseType::Research,
Some(now() + 200),
));
let expired = reg.expired_grants(now() + 150);
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].invention_id, "inv-001");
let expired = reg.expired_grants(now() + 300);
assert_eq!(expired.len(), 2);
}
#[test]
fn non_existent_invention_is_not_open_source() {
let reg = LicenseRegistry::new();
assert!(!reg.is_open_source("inv-nonexistent"));
}
#[test]
fn commercial_invention_is_not_open_source() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-001",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
assert!(!reg.is_open_source("inv-001"));
}
#[test]
fn protect_zome_accumulates() {
let mut reg = LicenseRegistry::new();
reg.protect_zome("z", vec!["inv-001".into()]);
reg.protect_zome("z", vec!["inv-002".into()]);
let required = reg.protected_zomes.get("z").unwrap();
assert_eq!(required.len(), 2);
}
#[test]
fn mixed_oss_and_commercial_requires_commercial_grant() {
let mut reg = LicenseRegistry::new();
reg.register_grant(make_grant(
"inv-oss",
"did:mycelix:community",
GrantedLicenseType::OpenSource,
None,
));
reg.register_grant(make_grant(
"inv-com",
"did:mycelix:alice",
GrantedLicenseType::Commercial,
None,
));
reg.protect_zome("hybrid_zome", vec!["inv-oss".into(), "inv-com".into()]);
assert_eq!(
reg.check_license("did:mycelix:alice", "hybrid_zome", now()),
LicenseCheckResult::Allowed,
);
assert_eq!(
reg.check_license("did:mycelix:bob", "hybrid_zome", now()),
LicenseCheckResult::RequiresLicense {
invention_id: "inv-com".into()
},
);
}
#[test]
fn grant_serde_roundtrip() {
let grant = LicenseGrant {
invention_id: "inv-001".into(),
licensee_did: "did:mycelix:alice".into(),
license_type: GrantedLicenseType::Commercial,
granted_at: now(),
expires_at: Some(now() + 1_000_000),
attribution_required: true,
royalty_percentage: Some(5.0),
};
let json = serde_json::to_string(&grant).unwrap();
let round: LicenseGrant = serde_json::from_str(&json).unwrap();
assert_eq!(round.invention_id, "inv-001");
assert_eq!(round.license_type, GrantedLicenseType::Commercial);
assert!(round.attribution_required);
assert_eq!(round.royalty_percentage, Some(5.0));
}
#[test]
fn attribution_and_royalty_preserved() {
let mut reg = LicenseRegistry::new();
reg.register_grant(LicenseGrant {
invention_id: "inv-001".into(),
licensee_did: "did:mycelix:alice".into(),
license_type: GrantedLicenseType::Commercial,
granted_at: now(),
expires_at: None,
attribution_required: true,
royalty_percentage: Some(2.5),
});
let grants = reg.get_grants_for_agent("did:mycelix:alice");
assert_eq!(grants.len(), 1);
assert!(grants[0].attribution_required);
assert_eq!(grants[0].royalty_percentage, Some(2.5));
}
}