use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use super::map::{AddResult, GraphMap, MapNodeId, MapState};
use crate::actions::{Action, ActionParams};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionNodeData {
pub action_name: String,
pub target: Option<String>,
pub args: HashMap<String, String>,
pub discovery: Option<serde_json::Value>,
}
impl ActionNodeData {
pub fn new(action_name: impl Into<String>) -> Self {
Self {
action_name: action_name.into(),
target: None,
args: HashMap::new(),
discovery: None,
}
}
pub fn with_target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.args.insert(key.into(), value.into());
self
}
pub fn with_discovery(mut self, discovery: serde_json::Value) -> Self {
self.discovery = Some(discovery);
self
}
pub fn from_input(input: &dyn MutationInput) -> Self {
let discovery = match input.result() {
ExplorationResult::Discover(children) => Some(serde_json::Value::Array(
children
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
)),
_ => None,
};
Self {
action_name: input.action_name().to_string(),
target: input.target().map(|s| s.to_string()),
args: HashMap::new(),
discovery,
}
}
}
impl From<&ActionNodeData> for Action {
fn from(data: &ActionNodeData) -> Self {
Action {
name: data.action_name.clone(),
params: ActionParams {
target: data.target.clone(),
args: data.args.clone(),
data: Vec::new(),
},
}
}
}
impl From<ActionNodeData> for Action {
fn from(data: ActionNodeData) -> Self {
Action {
name: data.action_name,
params: ActionParams {
target: data.target,
args: data.args,
data: Vec::new(),
},
}
}
}
pub trait ActionExtractor {
fn action_name(&self) -> &str;
fn target(&self) -> Option<&str>;
fn extract(&self) -> (&str, Option<&str>) {
(self.action_name(), self.target())
}
}
impl ActionExtractor for ActionNodeData {
fn action_name(&self) -> &str {
&self.action_name
}
fn target(&self) -> Option<&str> {
self.target.as_deref()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExplorationResult {
Discover(Vec<String>),
Success,
Fail(Option<String>),
}
impl ExplorationResult {
pub fn is_success(&self) -> bool {
matches!(
self,
ExplorationResult::Discover(_) | ExplorationResult::Success
)
}
pub fn is_fail(&self) -> bool {
matches!(self, ExplorationResult::Fail(_))
}
pub fn children(&self) -> Option<&[String]> {
match self {
ExplorationResult::Discover(children) => Some(children),
_ => None,
}
}
pub fn error(&self) -> Option<&str> {
match self {
ExplorationResult::Fail(Some(msg)) => Some(msg),
_ => None,
}
}
}
pub trait MutationInput {
fn node_id(&self) -> MapNodeId;
fn action_name(&self) -> &str;
fn target(&self) -> Option<&str>;
fn result(&self) -> &ExplorationResult;
fn dedup_key(&self) -> String {
let target = self.target().unwrap_or("_no_target_");
format!("{}:{}", self.action_name(), target)
}
}
#[derive(Debug, Clone)]
pub enum MapUpdate<N, E, S: MapState> {
AddChild {
parent: MapNodeId,
edge_data: E,
node_data: N,
node_state: S,
dedup_key: String,
},
Close(MapNodeId),
Noop,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MapUpdateResult {
NodeCreated(MapNodeId),
NodeClosed(MapNodeId),
Ignored,
Error(String),
}
impl<N, E, S> GraphMap<N, E, S>
where
N: Debug + Clone,
E: Debug + Clone,
S: MapState,
{
pub fn apply_update<K, F>(&mut self, update: MapUpdate<N, E, S>, key_fn: F) -> MapUpdateResult
where
K: Hash,
F: Fn(&str) -> K,
{
match update {
MapUpdate::AddChild {
parent,
edge_data,
node_data,
node_state,
dedup_key,
} => {
let key = key_fn(&dedup_key);
match self.add_child_if_absent(parent, edge_data, node_data, node_state, |_| key) {
Ok(AddResult::Added(id)) => MapUpdateResult::NodeCreated(id),
Ok(AddResult::AlreadyExists(_)) => MapUpdateResult::Ignored,
Err(e) => MapUpdateResult::Error(e.to_string()),
}
}
MapUpdate::Close(node_id) => {
self.close_with_cascade_up(node_id);
MapUpdateResult::NodeClosed(node_id)
}
MapUpdate::Noop => MapUpdateResult::Ignored,
}
}
pub fn apply_updates<K, F>(
&mut self,
updates: Vec<MapUpdate<N, E, S>>,
key_fn: F,
) -> Vec<MapUpdateResult>
where
K: Hash + Clone,
F: Fn(&str) -> K,
{
updates
.into_iter()
.map(|u| self.apply_update(u, &key_fn))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exploration::map::{ExplorationMap, MapNodeState};
struct TestInput {
node_id: MapNodeId,
action_name: String,
target: Option<String>,
result: ExplorationResult,
}
impl MutationInput for TestInput {
fn node_id(&self) -> MapNodeId {
self.node_id
}
fn action_name(&self) -> &str {
&self.action_name
}
fn target(&self) -> Option<&str> {
self.target.as_deref()
}
fn result(&self) -> &ExplorationResult {
&self.result
}
}
fn make_test_input(
node_id: u64,
action_name: &str,
target: Option<&str>,
success: bool,
) -> TestInput {
let result = if success {
ExplorationResult::Success
} else {
ExplorationResult::Fail(None)
};
TestInput {
node_id: MapNodeId::new(node_id),
action_name: action_name.to_string(),
target: target.map(|s| s.to_string()),
result,
}
}
#[test]
fn test_mutation_input_dedup_key() {
let input = make_test_input(0, "grep", Some("src/auth.rs"), true);
assert_eq!(input.dedup_key(), "grep:src/auth.rs");
}
#[test]
fn test_mutation_input_dedup_key_no_target() {
let input = make_test_input(0, "list", None, true);
assert_eq!(input.dedup_key(), "list:_no_target_");
}
#[test]
fn test_map_update_add_child() {
let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
let root = map.create_root("root".to_string(), MapNodeState::Open);
let update = MapUpdate::AddChild {
parent: root,
edge_data: "edge-1".to_string(),
node_data: "child-1".to_string(),
node_state: MapNodeState::Open,
dedup_key: "child-1".to_string(),
};
let result = map.apply_update(update, |k| k.to_string());
assert!(matches!(result, MapUpdateResult::NodeCreated(_)));
assert_eq!(map.node_count(), 2);
}
#[test]
fn test_map_update_add_child_dedup() {
let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
let root = map.create_root("root".to_string(), MapNodeState::Open);
let update1 = MapUpdate::AddChild {
parent: root,
edge_data: "edge-1".to_string(),
node_data: "child-1".to_string(),
node_state: MapNodeState::Open,
dedup_key: "same-key".to_string(),
};
let result1 = map.apply_update(update1, |k| k.to_string());
assert!(matches!(result1, MapUpdateResult::NodeCreated(_)));
let update2 = MapUpdate::AddChild {
parent: root,
edge_data: "edge-2".to_string(),
node_data: "child-2".to_string(),
node_state: MapNodeState::Open,
dedup_key: "same-key".to_string(),
};
let result2 = map.apply_update(update2, |k| k.to_string());
assert!(matches!(result2, MapUpdateResult::Ignored));
assert_eq!(map.node_count(), 2); }
#[test]
fn test_map_update_close() {
let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
let root = map.create_root("root".to_string(), MapNodeState::Open);
let update = MapUpdate::<String, String, MapNodeState>::Close(root);
let result = map.apply_update(update, |k| k.to_string());
assert!(matches!(result, MapUpdateResult::NodeClosed(_)));
assert!(map.frontiers().is_empty());
}
#[test]
fn test_map_update_noop() {
let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
let _root = map.create_root("root".to_string(), MapNodeState::Open);
let update = MapUpdate::<String, String, MapNodeState>::Noop;
let result = map.apply_update(update, |k| k.to_string());
assert!(matches!(result, MapUpdateResult::Ignored));
}
#[test]
fn test_apply_updates_batch() {
let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
let root = map.create_root("root".to_string(), MapNodeState::Open);
let updates = vec![
MapUpdate::AddChild {
parent: root,
edge_data: "e1".to_string(),
node_data: "c1".to_string(),
node_state: MapNodeState::Open,
dedup_key: "c1".to_string(),
},
MapUpdate::AddChild {
parent: root,
edge_data: "e2".to_string(),
node_data: "c2".to_string(),
node_state: MapNodeState::Open,
dedup_key: "c2".to_string(),
},
MapUpdate::Noop,
];
let results = map.apply_updates(updates, |k| k.to_string());
assert_eq!(results.len(), 3);
assert!(matches!(results[0], MapUpdateResult::NodeCreated(_)));
assert!(matches!(results[1], MapUpdateResult::NodeCreated(_)));
assert!(matches!(results[2], MapUpdateResult::Ignored));
assert_eq!(map.node_count(), 3);
}
}