171 lines
4.7 KiB
Rust
171 lines
4.7 KiB
Rust
|
|
use std::collections::BTreeMap;
|
||
|
|
use std::fmt;
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
pub const MAX_VALUE_DEPTH: usize = 32;
|
||
|
|
pub const MAX_VALUE_NODES: usize = 4096;
|
||
|
|
pub const MAX_VALUE_BYTES: usize = 1024 * 1024;
|
||
|
|
|
||
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||
|
|
#[serde(try_from = "String", into = "String")]
|
||
|
|
pub struct ActorRef([u8; 21]);
|
||
|
|
|
||
|
|
impl ActorRef {
|
||
|
|
pub fn new() -> Self {
|
||
|
|
Self::parse(&nanoid::nanoid!()).expect("Nano ID uses the supported alphabet")
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn as_str(&self) -> &str {
|
||
|
|
std::str::from_utf8(&self.0).expect("Nano ID is valid UTF-8")
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn parse(value: &str) -> Option<Self> {
|
||
|
|
if value.len() != 21
|
||
|
|
|| !value
|
||
|
|
.bytes()
|
||
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||
|
|
{
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let mut bytes = [0; 21];
|
||
|
|
bytes.copy_from_slice(value.as_bytes());
|
||
|
|
Some(Self(bytes))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for ActorRef {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<String> for ActorRef {
|
||
|
|
type Error = String;
|
||
|
|
|
||
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||
|
|
Self::parse(&value)
|
||
|
|
.ok_or_else(|| "actor reference must be a 21-character URL-safe NanoID".to_owned())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<ActorRef> for String {
|
||
|
|
fn from(value: ActorRef) -> Self {
|
||
|
|
value.to_string()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Debug for ActorRef {
|
||
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
write!(formatter, "@{}", self.as_str())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Display for ActorRef {
|
||
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
formatter.write_str(self.as_str())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Owned actor data, never a shared Rune heap object or execution handle.
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
#[serde(
|
||
|
|
tag = "type",
|
||
|
|
content = "data",
|
||
|
|
rename_all = "snake_case",
|
||
|
|
deny_unknown_fields
|
||
|
|
)]
|
||
|
|
pub enum Value {
|
||
|
|
Null,
|
||
|
|
Bool(bool),
|
||
|
|
Integer(i64),
|
||
|
|
String(String),
|
||
|
|
List(Vec<Value>),
|
||
|
|
Record(BTreeMap<String, Value>),
|
||
|
|
ActorRef(ActorRef),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Value {
|
||
|
|
pub fn validate(&self) -> Result<(), String> {
|
||
|
|
fn visit(
|
||
|
|
value: &Value,
|
||
|
|
depth: usize,
|
||
|
|
nodes: &mut usize,
|
||
|
|
bytes: &mut usize,
|
||
|
|
) -> Result<(), String> {
|
||
|
|
*nodes += 1;
|
||
|
|
if depth > MAX_VALUE_DEPTH || *nodes > MAX_VALUE_NODES {
|
||
|
|
return Err("actor value exceeds depth or node limit".to_owned());
|
||
|
|
}
|
||
|
|
match value {
|
||
|
|
Value::String(value) => *bytes += value.len(),
|
||
|
|
Value::List(values) => {
|
||
|
|
for value in values {
|
||
|
|
visit(value, depth + 1, nodes, bytes)?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Value::Record(values) => {
|
||
|
|
for (key, value) in values {
|
||
|
|
*bytes += key.len();
|
||
|
|
visit(value, depth + 1, nodes, bytes)?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => {}
|
||
|
|
}
|
||
|
|
if *bytes > MAX_VALUE_BYTES {
|
||
|
|
return Err("actor value exceeds string byte limit".to_owned());
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
visit(self, 0, &mut 0, &mut 0)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn generated_references_round_trip_and_are_distinct_values() {
|
||
|
|
let reference = ActorRef::new();
|
||
|
|
assert_eq!(ActorRef::parse(reference.as_str()), Some(reference));
|
||
|
|
assert_eq!(format!("{reference:?}"), format!("@{reference}"));
|
||
|
|
assert_ne!(
|
||
|
|
Value::ActorRef(reference),
|
||
|
|
Value::String(reference.to_string())
|
||
|
|
);
|
||
|
|
assert!(ActorRef::parse("too-short").is_none());
|
||
|
|
assert!(ActorRef::parse("abcdefghijklmnopqrst!").is_none());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn value_limits_are_inclusive() {
|
||
|
|
let mut value = Value::Null;
|
||
|
|
for _ in 0..MAX_VALUE_DEPTH {
|
||
|
|
value = Value::List(vec![value]);
|
||
|
|
}
|
||
|
|
assert!(value.validate().is_ok());
|
||
|
|
assert!(Value::List(vec![value]).validate().is_err());
|
||
|
|
assert!(
|
||
|
|
Value::List(vec![Value::Null; MAX_VALUE_NODES - 1])
|
||
|
|
.validate()
|
||
|
|
.is_ok()
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
Value::List(vec![Value::Null; MAX_VALUE_NODES])
|
||
|
|
.validate()
|
||
|
|
.is_err()
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
Value::String("x".repeat(MAX_VALUE_BYTES))
|
||
|
|
.validate()
|
||
|
|
.is_ok()
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
Value::String("x".repeat(MAX_VALUE_BYTES + 1))
|
||
|
|
.validate()
|
||
|
|
.is_err()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|