runa/crates/runa-core/src/value.rs

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()
);
}
}