use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::cap::Cap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Operation {
name: String,
requires: Vec<Cap>,
}
impl Operation {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn requires(&self) -> &[Cap] {
&self.requires
}
#[must_use]
pub fn missing(&self, held: &BTreeSet<Cap>) -> Option<&Cap> {
self.requires.iter().find(|cap| !held.contains(cap))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Surface {
tenant: String,
operations: BTreeMap<String, Operation>,
held: BTreeSet<Cap>,
}
impl Surface {
pub fn builder(tenant: &str) -> Result<SurfaceBuilder, SurfaceError> {
let tenant = crate::script::Tenant::new(tenant)
.map_err(|error| SurfaceError::InvalidTenant(error.to_string()))?;
Ok(SurfaceBuilder {
tenant,
operations: BTreeMap::new(),
held: BTreeSet::new(),
})
}
#[must_use]
pub fn tenant(&self) -> &str {
&self.tenant
}
#[must_use]
pub fn operation(&self, name: &str) -> Option<&Operation> {
self.operations.get(name)
}
#[must_use]
pub fn registered(&self) -> usize {
self.operations.len()
}
#[must_use]
pub fn holds(&self, cap: &Cap) -> bool {
self.held.contains(cap)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.operations.keys().map(String::as_str)
}
pub fn authorize(&self, name: &str) -> Result<&Operation, crate::proposal::Refusal> {
use crate::proposal::Refusal;
let operation = self
.operations
.get(name)
.ok_or_else(|| Refusal::UnknownOperation {
name: name.to_owned(),
})?;
match operation.missing(&self.held) {
Some(required) => Err(Refusal::CapabilityNotHeld {
operation: name.to_owned(),
required: required.as_str().to_owned(),
}),
None => Ok(operation),
}
}
}
#[derive(Debug)]
pub struct SurfaceBuilder {
tenant: crate::script::Tenant,
operations: BTreeMap<String, Operation>,
held: BTreeSet<Cap>,
}
impl SurfaceBuilder {
pub fn operation(mut self, name: &str, requires: &[Cap]) -> Result<Self, SurfaceError> {
if !is_operation_name(name) {
let refusal = Err(SurfaceError::InvalidOperation {
name: name.to_owned(),
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "operation: returning an error to the caller");
return refusal;
}
self.operations.insert(
name.to_owned(),
Operation {
name: name.to_owned(),
requires: requires.to_vec(),
},
);
Ok(self)
}
#[must_use]
pub fn holding(mut self, caps: &[Cap]) -> Self {
self.held.extend(caps.iter().cloned());
self
}
#[must_use]
pub fn holding_all_shipped(self) -> Self {
self.holding(&[Cap::net(), Cap::fs(), Cap::sys(), Cap::notify()])
}
#[must_use]
pub fn build(self) -> Surface {
Surface {
tenant: self.tenant.as_str().to_owned(),
operations: self.operations,
held: self.held,
}
}
}
fn is_operation_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= crate::proposal::MAX_FIELD_BYTES
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"-_.:".contains(&byte))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SurfaceError {
InvalidTenant(String),
InvalidOperation {
name: String,
},
}
impl fmt::Display for SurfaceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::InvalidTenant(ref reason) => write!(formatter, "InvalidTenant: {reason}"),
Self::InvalidOperation { ref name } => write!(
formatter,
"InvalidOperation: {name:?} is not an ASCII operation name of 1..={} bytes",
crate::proposal::MAX_FIELD_BYTES
),
}
}
}
impl std::error::Error for SurfaceError {}