use std::cell::RefCell;
use dot_parser::canonical::Graph;
use std::error::Error;
use std::time::Duration;
use log::{error, warn};
use crate::ProjectModel;
use crate::templates::{Actor, ActorDriver, Channel, ConsumePattern};
fn extract_type_name_from_edge_label(label_text: &str, from_node: &str, to_node: &str) -> String {
if let (Some(start), Some(end)) = (label_text.find('<'), label_text.find('>')) {
if start < end {
let type_name = &label_text[start + 1..end];
if type_name.chars()
.all(|ch| !ch.is_whitespace()) {
if let Some(c) = type_name.chars().next() {
if c.is_uppercase() {
return type_name.to_string();
}
}
}
}
}
let input = label_text.replace("\\n","\n").replace('"',"");
let first_line = input.lines().next();
if let Some(first_line) = first_line {
let parts: Vec<String> = first_line.split_whitespace()
.map(|s| {
let mut chars = s.chars();
match chars.next() {
Some(first_char) =>
{
if first_char.is_alphabetic() {
first_char.to_uppercase().collect::<String>() + chars.as_str()
} else {
format!("From{}To{}",from_node,to_node).to_string()
}
}
,
None => s.to_string(),
}
})
.collect();
if let Some(x) = parts.first() {
if !x.is_empty() {
let joined = parts.join("");
return joined;
}
}
}
format!("From{}To{}",from_node,to_node).to_string()
}
fn extract_capacity_from_edge_label(label_text: &str, default: usize) -> usize {
if let Some(start) = label_text.find('#') {
let remaining = &label_text[start + 1..];
if let Some(end) = remaining.find(|c: char| !c.is_ascii_digit()) {
remaining[..end].parse::<usize>().unwrap_or(default)
} else {
remaining.parse::<usize>().unwrap_or(default)
}
} else {
default }
}
fn extract_module_name(node_id: &str, label_text: &str) -> String {
let module_prefix = "mod::";
if let Some(start) = label_text.find(module_prefix) {
let remaining = &label_text[start + module_prefix.len()..];
if let Some(end) = remaining.find(|c: char| c == ',' || c.is_whitespace()) {
remaining[..end].to_string()
} else {
remaining.to_string()
}
} else {
let result = to_snake_case(node_id);
format!("mod_{}", result)
}
}
fn to_snake_case(input: &str) -> String {
let mut result = String::new();
for (i, c) in input.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
if let Some(lower) = c.to_lowercase().next() {
result.push(lower);
}
}
result
}
fn extract_consume_pattern_from_label(label: &str) -> ConsumePattern {
if label.contains(">>PeekCopy") {
ConsumePattern::PeekCopy
} else if label.contains(">>TakeCopy") {
ConsumePattern::TakeCopy
} else { ConsumePattern::Take
}
}
fn find_start_position(label: &str) -> usize {
let keywords = ["AtMostEvery(", "AtLeastEvery(", "OnEvent(", "OnCapacity(", "Other("];
keywords.iter()
.filter_map(|&keyword| label.find(keyword))
.min() .unwrap_or(label.len()) }
fn extract_actor_driver_from_label(label: &str) -> Vec<ActorDriver> {
let start_pos = find_start_position(label);
let mut result: Vec<ActorDriver> =
label[start_pos..].split("&&").filter_map(|part| {
let part = part.trim();
if part.starts_with("AtMostEvery") { part.strip_prefix("AtMostEvery(")
.and_then(|s| s.strip_suffix("ms)"))
.and_then(|ms| ms.trim().parse::<u64>().ok())
.map(Duration::from_millis)
.map(ActorDriver::AtMostEvery)
} else if part.starts_with("AtLeastEvery") { part.strip_prefix("AtLeastEvery(")
.and_then(|s| s.strip_suffix("ms)"))
.and_then(|ms| ms.trim().parse::<u64>().ok())
.map(Duration::from_millis)
.map(ActorDriver::AtLeastEvery)
} else if part.starts_with("OnEvent") {
if let Some(parts) = parse_parts(part, "OnEvent") {
Some(ActorDriver::EventDriven(parts))
} else {
warn!("Failed to parse OnEvent driver: {}", part);
let _ = parse_parts(part, "OnEvent");
None
}
} else if part.starts_with("OnCapacity") {
if let Some(parts) = parse_parts(part, "OnCapacity") {
Some(ActorDriver::CapacityDriven(parts))
} else {
warn!("Failed to parse OnCapacity driver: {}", part);
None
}
} else if part.starts_with("Other") {
part.strip_prefix("Other(")
.and_then(|s| s.strip_suffix(')'))
.map(|items| items.split(',').map(|item| item.trim().to_string()).collect())
.map(ActorDriver::Other)
} else {
None
}
}).collect();
if result.is_empty() {
result.push(ActorDriver::AtMostEvery(Duration::from_secs(1)));
}
result
}
fn parse_parts(part: &str, prefix: &str) -> Option<Vec<Vec<String>>> {
part.strip_prefix(prefix)
.and_then(|s| s.split_once('('))
.and_then(|(_, rest)| rest.split_once(')'))
.map(|(content,_)| {
let v: Vec<Vec<String>> = content.split("||").filter_map(|text| {
if text.is_empty() {
None
} else {
let parts: Vec<_> = text.split(':').map(|s| s.to_string()).collect();
Some(parts)
}
}).collect();
v
}
).filter(|v| !v.is_empty())
}
fn extract_channel_name(label_text: &str, from_node: &str, to_node: &str) -> String {
let module_prefix = "name::";
if let Some(start) = label_text.find(module_prefix) {
let remaining = &label_text[start + module_prefix.len()..];
if let Some(end) = remaining.find(|c: char| c == ',' || c.is_whitespace()) {
remaining[..end].to_string()
} else {
remaining.to_string()
}
} else {
format!("{}_to_{}", to_snake_case(from_node), to_snake_case(to_node))
}
}
pub(crate) fn extract_project_model<'a>(name: &str, g: Graph<'a, (&'a str, &'a str)>) -> Result<ProjectModel, Box<dyn Error>> {
let mut pm = ProjectModel { name: name.to_string(), ..Default::default() };
let mut nodes:Vec<(&str,&str)> = g.nodes.set.iter()
.map(|n| (n.1.id, n.1.attr.elems.iter()
.find_map(|(key, value)| if "label".eq(*key) { Some(*value) } else { None }).unwrap_or_default()
)
)
.collect();
nodes.sort(); for (id,label_text) in nodes {
let mod_name = extract_module_name(id, label_text);
let actor = Actor {
display_name: id.to_string(), mod_name,
rx_channels: Vec::new(), tx_channels: Vec::new(), driver: extract_actor_driver_from_label(label_text),
};
pm.actors.push(actor);
}
let mut edges:Vec<(&str,&str,&str)> = g.edges.set.iter()
.map(|e| (e.from, e.to, e.attr.elems
.iter()
.find_map(|(key, value)| if "label".eq(*key) { Some(*value) } else { None }).unwrap_or_default()
))
.collect();
edges.sort(); for (from,to,label_text) in edges {
let type_name = extract_type_name_from_edge_label(label_text, from, to);
let capacity = extract_capacity_from_edge_label(label_text, 8);
let name = extract_channel_name(label_text, from, to);
let consume_pattern = extract_consume_pattern_from_label(label_text);
if let Some(mod_name) = pm.actors.iter().filter(|f| f.display_name == from).map(|a| a.mod_name.clone()).next() {
let to_mod = pm.actors.iter().filter(|f| f.display_name == to).map(|a| a.mod_name.clone()).next().unwrap_or("unknown".into());
let mut channel = Channel {
name,
from_mod: mod_name,
to_mod,
batch_read: 1, batch_write: 1, message_type: type_name,
peek: consume_pattern == ConsumePattern::PeekCopy,
copy: consume_pattern != ConsumePattern::Take,
capacity,
bundle_index: -1,
rebundle_index: -1,
to_node: to.into(),
from_node: from.into(),
bundle_on_from: RefCell::new(true),
is_unbundled: false,
};
if let Some(a) = pm.actors.iter_mut().find(|f| f.display_name == from) {
a.driver.iter().for_each(|f| {
if let ActorDriver::CapacityDriven(pairs) = f {
pairs.iter()
.filter(|v| v[0].eq(&channel.name))
.for_each(|v| channel.batch_write = v[1].parse().expect("expected int") );
};
});
let mut tx_channel = channel.clone();
let tx_counter_index = pm.channels.iter().filter(|f|
f[0].name.eq(&channel.name)
).map(|f| f.iter()
.filter(|f| f.to_node.eq(&tx_channel.to_node) )
.count()).max().unwrap_or(0);
tx_channel.bundle_index = tx_counter_index as isize;
roll_up_bundle(&mut a.tx_channels, tx_channel, |_t,_v| true);
}
if let Some(a) = pm.actors.iter_mut().find(|f| f.display_name == to) {
a.driver.iter().for_each(|f| {
if let ActorDriver::EventDriven(pairs) = f {
pairs.iter()
.filter(|v| v[0].eq(&channel.name))
.for_each(|v| channel.batch_read = v[1].parse().expect("expected int") );
};
});
let mut rx_channel = channel.clone();
let rx_counter_index = pm.channels.iter().filter(|f|
f[0].name.eq(&channel.name)
).map(|f| f.iter()
.filter(|f| f.from_node.eq(&rx_channel.from_node) )
.count()).max().unwrap_or(0);
rx_channel.rebundle_index = rx_counter_index as isize; rx_channel.bundle_index = rx_counter_index as isize;
roll_up_bundle(&mut a.rx_channels, rx_channel, |_t,_v| true);
}
roll_up_bundle(&mut pm.channels, channel, |t,v| v.iter().all(|g| g.from_node.eq(&t.from_node) ) );
} else {
error!("Failed to find actor with id: {}", from);
}
}
let mut new_channels:Vec<Vec<Channel>> = Vec::new();
pm.channels.into_iter().for_each(|mut c| {
if c.len()>1 {
new_channels.push(c);
} else if let Some(local) = c.pop() {
roll_up_bundle(&mut new_channels, local, |t,v| {
let result = v.iter().all(|g| g.to_node.eq(&t.to_node));
if result {
v.iter().for_each(|f| { f.bundle_on_from.replace(false); });
}
result
});
}
});
pm.channels = new_channels;
pm.channels.iter().for_each(|c| {
if let Some(a) = pm.actors.iter_mut().find(|f| f.display_name.eq(&c[0].from_node)) {
if let Some(x) = a.tx_channels.iter_mut().find(|f| f[0].name.eq(&c[0].name)) {
x.iter_mut().for_each(|f| {
f.bundle_on_from.clone_from(&c[0].bundle_on_from);
f.is_unbundled = c.len()==1;
} );
}
}
if let Some(a) = pm.actors.iter_mut().find(|f| f.display_name.eq(&c[0].to_node)) {
if let Some(x) = a.rx_channels.iter_mut().find(|f| f[0].name.eq(&c[0].name)) {
x.iter_mut().for_each(|f| {
f.bundle_on_from.clone_from(&c[0].bundle_on_from);
f.is_unbundled = c.len()==1;
} );
}
}
});
Ok(pm)
}
fn roll_up_bundle(collection: &mut Vec<Vec<Channel>>, mut insert_me: Channel, group_by: fn(&Channel, &Vec<Channel>) -> bool) {
if let Some(x) = collection.iter_mut().find(|f| {
f[0].name.eq(&insert_me.name) && f[0].capacity.eq(&insert_me.capacity) && f[0].message_type.eq(&insert_me.message_type) && group_by(&insert_me, f)
}
) {
if insert_me.capacity > x[0].capacity {
x.iter_mut().for_each(|f| f.capacity = insert_me.capacity );
} else {
insert_me.capacity = x[0].capacity;
}
if !insert_me.from_mod.eq(&x[0].from_mod) {
insert_me.from_mod.clone_from(&x[0].from_mod);
}
if insert_me.copy != x[0].copy {
if x[0].copy {
insert_me.copy = true;
} else {
x.iter_mut().for_each(|f| f.copy = true );
}
}
x.push(insert_me); x.iter_mut().for_each(|f| f.bundle_index = -1 );
} else {
collection.push(vec![insert_me]);
}
}
#[cfg(test)]
mod tests {
use crate::extract_details;
use crate::extract_details::*;
#[test]
fn test_extract_type_name_from_edge_label() {
let label = "IMAP server details\nemail, password";
let from = "ConfigLoader";
let to = "IMAPClient";
let result = extract_details::extract_type_name_from_edge_label(label, from, to);
assert_eq!(result, "IMAPServerDetails".to_string());
}
#[test]
fn test_extract_type_name_from_edge_label2() {
let label = "<Widget>#1024";
assert_eq!(extract_type_name_from_edge_label(label, "NodeA", "NodeB"), "Widget".to_string());
let label_with_junk = "Some text <WidgetType>#512 more text";
assert_eq!(extract_type_name_from_edge_label(label_with_junk, "NodeA", "NodeB"), "WidgetType".to_string());
let label_missing_type = "#1024";
assert_eq!(extract_type_name_from_edge_label(label_missing_type, "NodeA", "NodeB"), "FromNodeAToNodeB".to_string());
}
#[test]
fn test_extract_capacity_from_edge_label() {
let label = "Capacity #1024";
assert_eq!(extract_capacity_from_edge_label(label, 512), 1024);
let label_missing_capacity = "No capacity here";
assert_eq!(extract_capacity_from_edge_label(label_missing_capacity, 512), 512);
}
#[test]
fn test_extract_module_name() {
let label = "mod::MyModule";
assert_eq!(extract_module_name("NodeA", label), "MyModule");
let label_missing_module = "No module here";
assert_eq!(extract_module_name("NodeA", label_missing_module), "mod_node_a");
}
#[test]
fn test_extract_consume_pattern_from_label() {
let label_peek_copy = ">>PeekCopy something else";
assert_eq!(extract_consume_pattern_from_label(label_peek_copy), ConsumePattern::PeekCopy);
let label_take = ">>Take even more";
assert_eq!(extract_consume_pattern_from_label(label_take), ConsumePattern::Take);
let label_missing_pattern = "No pattern here";
assert_eq!(extract_consume_pattern_from_label(label_missing_pattern), ConsumePattern::Take);
}
#[test]
fn test_extract_actor_driver_from_label() {
let label = "AtLeastEvery(5000ms) && OnEvent(C1//10||B2//10) && OnCapacity(C2//20||A1//20)";
let drivers = extract_actor_driver_from_label(label);
assert!(drivers.contains(&ActorDriver::AtLeastEvery(Duration::from_millis(5000))));
assert!(drivers.iter().any(|d| matches!(d, ActorDriver::EventDriven(_))));
assert!(drivers.iter().any(|d| matches!(d, ActorDriver::CapacityDriven(_))));
}
#[test]
fn test_correct_format() {
let input = "Event(xx:1||y:1)";
let expected = Some(vec![vec!["xx".to_string(), "1".to_string()], vec!["y".to_string(), "1".to_string()]]);
assert_eq!(parse_parts(input, "Event"), expected);
}
#[test]
fn test_multiple_parts() {
let input = "Capacity(a:1:b:2||c:3:d:4)";
let expected = Some(vec![vec!["a".to_string(), "1".to_string(), "b".to_string(), "2".to_string()], vec!["c".to_string(), "3".to_string(), "d".to_string(), "4".to_string()]]);
assert_eq!(parse_parts(input, "Capacity"), expected);
}
#[test]
fn test_incorrect_prefix() {
let input = "Event(xx:1||y:1)";
assert_eq!(parse_parts(input, "Capacity"), None);
}
#[test]
fn test_missing_closing_parenthesis() {
let input = "Event(xx:1||y:1";
assert_eq!(parse_parts(input, "Event"), None);
}
#[test]
fn test_empty_content() {
let input = "Event()";
assert_eq!(parse_parts(input, "Event"), None);
}
#[test]
fn test_no_delimiters() {
let input = "Event(xxy1)";
let expected = Some(vec![vec!["xxy1".to_string()]]);
assert_eq!(parse_parts(input, "Event"), expected);
}
#[test]
fn test_example_1() {
let input = "OnEvent(client_request:1||feedback:1)";
let expected = Some(vec![
vec!["client_request".to_string(),"1".to_string()],
vec!["feedback".to_string(),"1".to_string()]
]);
assert_eq!(parse_parts(input, "OnEvent"), expected);
}
#[test]
fn test_example_2() {
let input = "OnEvent(pbft_message:1)";
let expected = Some(vec![vec!["pbft_message".to_string(),"1".to_string()]]);
assert_eq!(parse_parts(input, "OnEvent"), expected);
}
}