use std::collections::BTreeMap; use runa_protocol::{ActorSnapshotDto, StackFrame, Value}; use serde_json::{Value as Json, json}; #[derive(Default)] pub(crate) struct Variables { next: u64, values: BTreeMap, references: BTreeMap, root: Option, } impl Variables { pub fn clear(&mut self) { self.values.clear(); self.references.clear(); self.root = None; } fn reference(&mut self, value: Value, path: String) -> u64 { if !path.is_empty() { if let Some(reference) = self.references.get(&path) { return *reference; } self.references.insert(path.clone(), self.next + 1); } self.next += 1; self.values.insert(self.next, (value, path)); self.next } pub fn capture(&mut self, actor: &ActorSnapshotDto) { self.clear(); let mut metadata = plain(json!({ "mailbox_id": actor.mailbox_id, "implementation_id": actor.implementation_id, "incarnation": actor.incarnation, "behavior_path": actor.behavior_path, "status": serde_json::to_value(&actor.status).unwrap()["status"], })); if let Value::Record(entries) = &mut metadata { entries.insert( "actor_ref".to_owned(), runa_core::ActorRef::parse(&actor.actor_ref) .map(Value::ActorRef) .unwrap_or_else(|| Value::String(actor.actor_ref.clone())), ); if let runa_protocol::ActorStatusDto::Failed(message) = &actor.status { entries.insert("failure".to_owned(), Value::String(message.clone())); } } let value = record([ ("committed_state", actor.state.clone()), ( "mailbox", record([ ( "reserved", Value::List(actor.reserved.clone().into_iter().collect()), ), ("queued", Value::List(actor.queued.clone())), ]), ), ("config", plain(json!({"log_level": actor.log_level}))), ("metadata", metadata), ]); self.root = Some(self.reference(value, "actor".to_owned())); } pub fn scope(&self) -> Result { let reference = self .root .ok_or("Actor snapshot unavailable while running")?; Ok(json!({ "name": "Actor snapshot (committed; not frame locals)", "variablesReference": reference, "expensive": false, "namedVariables": 4 })) } pub fn frame_scope(&mut self, frame: &StackFrame) -> Json { let value = plain(serde_json::to_value(frame).unwrap()); let reference = self.reference(value, String::new()); json!({"name": "Frame metadata (not locals)", "variablesReference": reference, "expensive": false}) } pub fn variables(&mut self, args: &Json) -> Result { let reference = super::integer(args, "variablesReference", None)?; let (value, path) = self .values .get(&reference) .cloned() .ok_or("Stale or unknown variables reference")?; let indexed = matches!(value, Value::List(_)); match args.get("filter") { Some(Json::String(filter)) if (filter == "named" && indexed) || (filter == "indexed" && !indexed) => { return Ok(json!({"variables": []})); } None => {} Some(Json::String(filter)) if matches!(filter.as_str(), "named" | "indexed") => {} _ => return Err("Invalid variables filter".to_owned()), } let entries: Vec<_> = match value { Value::Record(entries) => entries.into_iter().collect(), Value::List(entries) => entries .into_iter() .enumerate() .map(|(index, value)| (index.to_string(), value)) .collect(), _ => return Err("Variable has no children".to_owned()), }; let start = super::integer(args, "start", Some(0))? as usize; let count = super::integer(args, "count", Some(0))? as usize; let variables: Vec<_> = entries .into_iter() .skip(start) .take(if count == 0 { usize::MAX } else { count }) .map(|(name, value)| { let child_path = if path.is_empty() { String::new() } else { format!("{path}/{}", name.replace('~', "~0").replace('/', "~1")) }; self.variable(&name, value, child_path) }) .collect(); Ok(json!({"variables": variables})) } pub fn evaluate(&mut self, args: &Json) -> Result { if let Some(context) = args.get("context") && !matches!(context.as_str(), Some("watch" | "repl" | "variables")) { return Err( "Snapshot paths are supported only in Watch, Debug Console and Variables" .to_owned(), ); } let expression = args["expression"] .as_str() .ok_or("Expected a snapshot path")?; let mut tokens = expression.split('/'); if expression.len() > 4096 || tokens.next() != Some("actor") || tokens.clone().count() > 64 { return Err( "Use actor or actor/; Rune expressions are unsupported" .to_owned(), ); } let root = self .root .ok_or("Actor snapshot unavailable while running")?; let mut value = self.values[&root].0.clone(); for token in tokens { let mut chars = token.chars(); let mut key = String::new(); while let Some(ch) = chars.next() { key.push(if ch == '~' { match chars.next() { Some('0') => '~', Some('1') => '/', _ => return Err("Invalid JSON Pointer escape; use ~0 or ~1".to_owned()), } } else { ch }); } value = match value { Value::Record(mut entries) => { entries.remove(&key).ok_or("Snapshot field not found")? } Value::List(entries) => { if key.is_empty() || (key.starts_with('0') && key.len() > 1) || !key.bytes().all(|byte| byte.is_ascii_digit()) { return Err("Invalid snapshot list index".to_owned()); } let index: usize = key.parse().map_err(|_| "Invalid snapshot list index")?; entries .get(index) .cloned() .ok_or("Snapshot list index out of range")? } _ => return Err("Snapshot path traverses a scalar value".to_owned()), }; } let mut variable = self.variable(expression, value, expression.to_owned()); variable["result"] = variable["value"].take(); let object = variable.as_object_mut().unwrap(); object.remove("name"); object.remove("value"); Ok(variable) } fn variable(&mut self, name: &str, value: Value, path: String) -> Json { let (kind, preview, children, indexed) = match &value { Value::Null => ("null", "null".to_owned(), 0, false), Value::Bool(value) => ("bool", value.to_string(), 0, false), Value::Integer(value) => ("integer", value.to_string(), 0, false), Value::String(value) => { let preview: String = value.chars().take(256).collect(); let preview = if preview.len() < value.len() { format!("{}… ({} bytes)", json!(preview), value.len()) } else { json!(preview).to_string() }; ("string", preview, 0, false) } Value::ActorRef(value) => ("actor_ref", format!("@{value}"), 0, false), Value::List(entries) => ( "list", format!("List({})", entries.len()), entries.len(), true, ), Value::Record(entries) => ( "record", format!("Record({})", entries.len()), entries.len(), false, ), }; let reference = if children == 0 { 0 } else { self.reference(value, path.clone()) }; let mut result = json!({ "name": name, "value": preview, "type": kind, "variablesReference": reference, "presentationHint": {"attributes": ["readOnly"]} }); if !path.is_empty() { result["evaluateName"] = json!(path); } if matches!(kind, "list" | "record") { result[if indexed { "indexedVariables" } else { "namedVariables" }] = json!(children); } result } } fn record(entries: [(&str, Value); N]) -> Value { Value::Record( entries .into_iter() .map(|(key, value)| (key.to_owned(), value)) .collect(), ) } fn plain(value: Json) -> Value { match value { Json::Null => Value::Null, Json::Bool(value) => Value::Bool(value), Json::Number(value) => value .as_i64() .map(Value::Integer) .unwrap_or_else(|| Value::String(value.to_string())), Json::String(value) => Value::String(value), Json::Array(values) => Value::List(values.into_iter().map(plain).collect()), Json::Object(values) => Value::Record( values .into_iter() .map(|(key, value)| (key, plain(value))) .collect(), ), } }