use super::budget::{check_amount, BudgetGrant};
use super::KnlResult;
pub type ScopeId = String;
fn mint_id() -> ScopeId {
uuid::Uuid::new_v4().to_string()
}
#[derive(Debug)]
pub struct Scope {
id: ScopeId,
owner: String,
grant: Option<BudgetGrant>,
}
impl Scope {
pub fn new(owner: String, grant: Option<BudgetGrant>) -> Self {
Self {
id: mint_id(),
owner,
grant,
}
}
pub(super) fn restore(id: Option<ScopeId>, owner: String, grant: Option<BudgetGrant>) -> Self {
Self {
id: id.unwrap_or_else(mint_id),
owner,
grant,
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn owner(&self) -> &str {
&self.owner
}
pub fn grant(&self) -> Option<&BudgetGrant> {
self.grant.as_ref()
}
pub(super) fn grant_more(&mut self, grant: BudgetGrant) -> KnlResult<()> {
check_amount(grant.amount)?;
self.grant = Some(grant);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::knl::ANON;
#[test]
fn every_scope_is_issued_its_own_id() {
let a = Scope::new(ANON.to_string(), None);
let b = Scope::new(ANON.to_string(), None);
assert!(!a.id().is_empty());
assert_ne!(a.id(), b.id(), "scope ids must be unique");
assert_eq!(a.owner(), ANON);
assert_eq!(a.grant(), None, "no grant, no ledger");
}
#[test]
fn restore_keeps_the_recorded_id_and_issues_one_when_there_is_none() {
let kept = Scope::restore(
Some("scope-from-the-log".to_string()),
"user-1".to_string(),
Some(BudgetGrant::new(100)),
);
assert_eq!(kept.id(), "scope-from-the-log");
assert_eq!(kept.owner(), "user-1");
assert_eq!(
kept.grant().map(|g| g.amount),
Some(100),
"the grant the log recorded comes back"
);
let minted = Scope::restore(None, ANON.to_string(), None);
assert!(
!minted.id().is_empty(),
"an older log still resumes under a scope id"
);
assert_ne!(minted.id(), kept.id());
}
#[test]
fn a_second_grant_replaces_the_words_and_refuses_a_negative_amount() {
let mut scope = Scope::new(
"user-2".to_string(),
Some(BudgetGrant {
amount: 100,
tag: Some("tokens".to_string()),
desc: None,
}),
);
assert_eq!(scope.grant().and_then(|g| g.tag.as_deref()), Some("tokens"));
scope
.grant_more(BudgetGrant {
amount: 5,
tag: Some("calls".to_string()),
desc: None,
})
.expect("a second grant");
assert_eq!(scope.grant().and_then(|g| g.tag.as_deref()), Some("calls"));
assert_eq!(scope.grant().map(|g| g.amount), Some(5));
let err = scope
.grant_more(BudgetGrant::new(-1))
.expect_err("a negative grant");
assert!(err.reason().contains("non-negative"), "{err}");
assert_eq!(
scope.grant().and_then(|g| g.tag.as_deref()),
Some("calls"),
"a refused grant leaves the words as they were"
);
}
}