use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use super::PlacementTypeError;
pub const MAX_ID_LEN: usize = 128;
pub const MAX_ID_SEGMENTS: usize = 8;
pub const MAX_ATTR_NAME_LEN: usize = 64;
pub const MAX_ATTRS: usize = 64;
pub const MAX_LIST_LEN: usize = 256;
pub const MAX_STR_LEN: usize = 1024;
pub const EXPERIMENTAL_PREFIX: &str = "x";
pub(crate) fn valid_segment(seg: &str) -> bool {
let mut chars = seg.chars();
match chars.next() {
Some(c) if c.is_ascii_lowercase() || c.is_ascii_digit() => {}
_ => return false,
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
}
pub(crate) fn validate_dotted(s: &str, min_segments: usize) -> Result<(), PlacementTypeError> {
let bad = |why: &str| PlacementTypeError::InvalidId {
id: truncate(s),
reason: why.to_string(),
};
if s.is_empty() {
return Err(bad("empty"));
}
if s.len() > MAX_ID_LEN {
return Err(bad("too long"));
}
let segs: Vec<&str> = s.split('.').collect();
if segs.len() < min_segments {
return Err(bad("too few dot-separated segments"));
}
if segs.len() > MAX_ID_SEGMENTS {
return Err(bad("too many dot-separated segments"));
}
if let Some(seg) = segs.iter().find(|seg| !valid_segment(seg)) {
return Err(bad(&format!(
"segment {seg:?} must match [a-z0-9][a-z0-9_-]*"
)));
}
Ok(())
}
fn truncate(s: &str) -> String {
s.chars().take(MAX_ID_LEN).collect()
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct CapabilityId(String);
impl CapabilityId {
pub fn new(s: impl Into<String>) -> Result<Self, PlacementTypeError> {
let s = s.into();
validate_dotted(&s, 2)?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn family(&self) -> &str {
self.0.split('.').next().unwrap_or_default()
}
pub fn is_experimental(&self) -> bool {
self.family() == EXPERIMENTAL_PREFIX
}
pub fn is_under(&self, prefix: &str) -> bool {
self.0 == prefix
|| (self.0.len() > prefix.len()
&& self.0.starts_with(prefix)
&& self.0.as_bytes()[prefix.len()] == b'.')
}
}
impl TryFrom<String> for CapabilityId {
type Error = PlacementTypeError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<CapabilityId> for String {
fn from(id: CapabilityId) -> Self {
id.0
}
}
impl fmt::Display for CapabilityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AttrValue {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
List(Vec<AttrValue>),
}
impl AttrValue {
pub fn as_f64(&self) -> Option<f64> {
match self {
AttrValue::Int(i) => Some(*i as f64),
AttrValue::Float(f) => Some(*f),
_ => None,
}
}
pub fn loose_eq(&self, other: &AttrValue) -> bool {
match (self.as_f64(), other.as_f64()) {
(Some(a), Some(b)) => a == b,
_ => match (self, other) {
(AttrValue::List(a), AttrValue::List(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.loose_eq(y))
}
_ => self == other,
},
}
}
pub(crate) fn validate(&self, depth: usize) -> Result<(), String> {
match self {
AttrValue::Float(f) if !f.is_finite() => Err("non-finite float".into()),
AttrValue::Str(s) if s.len() > MAX_STR_LEN => Err("string too long".into()),
AttrValue::List(_) if depth > 0 => Err("nested lists are not allowed".into()),
AttrValue::List(v) if v.len() > MAX_LIST_LEN => Err("list too long".into()),
AttrValue::List(v) => v.iter().try_for_each(|x| x.validate(depth + 1)),
_ => Ok(()),
}
}
}
impl From<bool> for AttrValue {
fn from(v: bool) -> Self {
AttrValue::Bool(v)
}
}
impl From<i64> for AttrValue {
fn from(v: i64) -> Self {
AttrValue::Int(v)
}
}
impl From<f64> for AttrValue {
fn from(v: f64) -> Self {
AttrValue::Float(v)
}
}
impl From<&str> for AttrValue {
fn from(v: &str) -> Self {
AttrValue::Str(v.to_string())
}
}
pub(crate) fn valid_attr_name(name: &str) -> bool {
let mut chars = name.chars();
name.len() <= MAX_ATTR_NAME_LEN
&& matches!(chars.next(), Some(c) if c.is_ascii_lowercase())
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provenance {
Claimed,
Probed,
Measured,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityState {
#[default]
Available,
Busy,
Reserved,
Degraded,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(try_from = "CapabilityRaw")]
pub struct Capability {
pub id: CapabilityId,
#[serde(default)]
pub attrs: BTreeMap<String, AttrValue>,
pub provenance: Provenance,
#[serde(default)]
pub state: CapabilityState,
#[serde(default)]
pub exclusive: bool,
}
#[derive(Serialize)]
struct CapabilityOut<'a> {
id: &'a CapabilityId,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
attrs: &'a BTreeMap<String, AttrValue>,
provenance: Provenance,
state: CapabilityState,
exclusive: bool,
}
impl Serialize for Capability {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.validate().map_err(serde::ser::Error::custom)?;
CapabilityOut {
id: &self.id,
attrs: &self.attrs,
provenance: self.provenance,
state: self.state,
exclusive: self.exclusive,
}
.serialize(s)
}
}
#[derive(Deserialize)]
struct CapabilityRaw {
id: CapabilityId,
#[serde(default)]
attrs: BTreeMap<String, AttrValue>,
provenance: Provenance,
#[serde(default)]
state: CapabilityState,
#[serde(default)]
exclusive: bool,
}
impl TryFrom<CapabilityRaw> for Capability {
type Error = PlacementTypeError;
fn try_from(r: CapabilityRaw) -> Result<Self, Self::Error> {
let cap = Capability {
id: r.id,
attrs: r.attrs,
provenance: r.provenance,
state: r.state,
exclusive: r.exclusive,
};
cap.validate()?;
Ok(cap)
}
}
impl Capability {
pub fn new(id: CapabilityId, provenance: Provenance) -> Self {
Self {
id,
attrs: BTreeMap::new(),
provenance,
state: CapabilityState::Available,
exclusive: false,
}
}
pub fn with_attr(mut self, name: &str, value: impl Into<AttrValue>) -> Self {
self.attrs.insert(name.to_string(), value.into());
self
}
pub fn try_with_attr(
self,
name: &str,
value: impl Into<AttrValue>,
) -> Result<Self, PlacementTypeError> {
let cap = self.with_attr(name, value);
cap.validate()?;
Ok(cap)
}
pub fn with_state(mut self, state: CapabilityState) -> Self {
self.state = state;
self
}
pub fn exclusive(mut self) -> Self {
self.exclusive = true;
self
}
pub fn validate(&self) -> Result<(), PlacementTypeError> {
if self.attrs.len() > MAX_ATTRS {
return Err(self.attr_err("*", "too many attributes"));
}
for (name, value) in &self.attrs {
if !valid_attr_name(name) {
return Err(self.attr_err(name, "name must match [a-z][a-z0-9_]*"));
}
value.validate(0).map_err(|why| self.attr_err(name, &why))?;
}
Ok(())
}
fn attr_err(&self, attr: &str, reason: &str) -> PlacementTypeError {
PlacementTypeError::InvalidAttr {
id: self.id.to_string(),
attr: attr.chars().take(MAX_ATTR_NAME_LEN).collect(),
reason: reason.to_string(),
}
}
}