use crate::{ast::extract_contract_info, types::*, utils::*};
use anyhow::Result;
use itertools::Itertools;
use serde_json::Value;
use std::collections::HashSet;
pub fn generate_sequence_diagram(ast: &Value, light_colors: bool) -> Result<String> {
let config = crate::Config {
light_colors,
output_file: None,
show_storage_updates: true,
};
generate_sequence_diagram_with_config(ast, config)
}
pub fn generate_sequence_diagram_with_config(ast: &Value, config: crate::Config) -> Result<String> {
let data = extract_contract_info(ast, config.show_storage_updates)?;
let mut diagram = Vec::new();
diagram.push("```mermaid".to_string());
diagram.push("sequenceDiagram".to_string());
diagram.push("title Smart Contract Interaction Sequence Diagram".to_string());
diagram.push("autonumber".to_string());
diagram.push("".to_string());
add_theme_config(&mut diagram, config.light_colors);
let ordered_participants = order_participants(&data.participants);
add_participants(&mut diagram, &ordered_participants, &data.contracts);
diagram.push("".to_string());
add_section_title(&mut diagram, "User Interactions", config.light_colors);
diagram.extend(data.user_interactions);
if !data.contract_interactions.is_empty() {
diagram.push("".to_string());
add_section_title(&mut diagram, "Contract-to-Contract Interactions", config.light_colors);
for (function_key, interactions_list) in data.contract_interactions.iter() {
if !interactions_list.is_empty() {
let parts: Vec<&str> = function_key.split('.').collect();
if parts.len() == 2 {
let (contract, function) = (parts[0], parts[1]);
diagram.push(format!("Note right of {}: Processing {}", contract, function));
diagram.extend(interactions_list.clone());
diagram.push("".to_string()); }
}
}
}
if !data.events.is_empty() {
diagram.push("".to_string());
add_section_title(&mut diagram, "Event Definitions", config.light_colors);
for (contract, event) in &data.events {
diagram.push(format!("Note over {},{}: Event: {}", contract, contract, event));
}
}
if !data.contracts.is_empty() {
diagram.push("".to_string());
add_section_title(&mut diagram, "Contract Relationships", config.light_colors);
for (contract_name, info) in &data.contracts {
if !info.functions.is_empty() {
let functions_str = info.functions.join(", ");
diagram.push(format!("Note over {}: Functions: {}", contract_name, functions_str));
}
}
diagram.push("".to_string());
for (contract_name, info) in &data.contracts {
if !info.inherits_from.is_empty() {
let bases_str = info.inherits_from.join(", ");
diagram
.push(format!("Note right of {}: Inherits from: {}", contract_name, bases_str));
}
}
diagram.push("".to_string());
for (contract_name, info) in &data.contracts {
if info.contract_type != "contract" {
diagram
.push(format!("Note right of {}: Type: {}", contract_name, info.contract_type));
}
}
if !data.contract_relationships.is_empty() {
diagram.push("".to_string());
let mut seen_relationships = HashSet::new();
for rel in &data.contract_relationships {
if rel.relation_type == "calls"
&& data.participants.contains(&rel.source)
&& data.participants.contains(&rel.target)
{
let rel_key = format!("{}->{}", rel.source, rel.target);
if !seen_relationships.contains(&rel_key) {
diagram.push(format!(
"Note right of {}: Interacts with {}",
rel.source, rel.target
));
seen_relationships.insert(rel_key);
}
}
}
}
}
add_legend(&mut diagram, config.light_colors);
diagram.push("```".to_string());
Ok(diagram.join("\n"))
}
fn add_theme_config(diagram: &mut Vec<String>, light_colors: bool) {
diagram.push("%%{init: {".to_string());
diagram.push(" 'theme': 'base',".to_string());
diagram.push(" 'themeVariables': {".to_string());
if light_colors {
diagram.push(" 'primaryColor': '#fafbfc',".to_string());
diagram.push(" 'primaryTextColor': '#444',".to_string());
diagram.push(" 'primaryBorderColor': '#e1e4e8',".to_string());
diagram.push(" 'lineColor': '#a0aec0',".to_string());
diagram.push(" 'secondaryColor': '#f5fbff',".to_string());
diagram.push(" 'tertiaryColor': '#fff8f8'".to_string());
} else {
diagram.push(" 'primaryColor': '#f5f5f5',".to_string());
diagram.push(" 'primaryTextColor': '#333',".to_string());
diagram.push(" 'primaryBorderColor': '#999',".to_string());
diagram.push(" 'lineColor': '#666',".to_string());
diagram.push(" 'secondaryColor': '#f0f8ff',".to_string());
diagram.push(" 'tertiaryColor': '#fff5f5'".to_string());
}
diagram.push(" }".to_string());
diagram.push("}}%%".to_string());
diagram.push("".to_string());
}
fn order_participants(participants: &HashSet<String>) -> Vec<String> {
let mut ordered = Vec::new();
if participants.contains("User") {
ordered.push("User".to_string());
}
for participant in participants.iter().sorted() {
if participant != "User" && participant != "Events" {
ordered.push(participant.clone());
}
}
if participants.contains("Events") {
ordered.push("Events".to_string());
}
ordered
}
fn add_participants(
diagram: &mut Vec<String>,
ordered_participants: &[String],
contracts: &std::collections::HashMap<String, ContractInfo>,
) {
for participant in ordered_participants {
if participant == "User" {
diagram.push("participant User as \"External User\"".to_string());
} else if participant == "Events" {
diagram.push("participant Events as \"Blockchain Events\"".to_string());
} else if participant == "TokenContract" {
diagram.push("participant TokenContract as \"ERC20/ERC721 Tokens\"".to_string());
} else {
if let Some(contract_info) = contracts.get(participant) {
let key_vars: Vec<&(String, String)> = contract_info
.variables
.iter()
.filter(|(name, _)| is_important_variable(name))
.collect();
let mut description_parts = Vec::new();
description_parts.push(participant.clone());
if contract_info.contract_type != "contract" {
description_parts[0] =
format!("{} ({})", participant, contract_info.contract_type);
}
if !key_vars.is_empty() {
let var_list: Vec<String> = key_vars
.iter()
.take(2)
.map(|(name, typ)| format!("{}: {}", name, typ))
.collect();
description_parts.push(format!("({})", var_list.join(", ")));
}
if !contract_info.source_file.is_empty() {
description_parts.push(format!("from {}", contract_info.source_file));
}
let title = description_parts.join("<br/>");
diagram.push(format!("participant {} as \"{}\"", participant, title));
} else {
diagram.push(format!("participant {}", participant));
}
}
}
}
fn add_section_title(diagram: &mut Vec<String>, title: &str, light_colors: bool) {
let color = if light_colors {
match title {
"User Interactions" => "rgb(252, 252, 255)",
"Contract-to-Contract Interactions" => "rgb(248, 252, 255)",
"Event Definitions" => "rgb(255, 252, 252)",
"Contract Relationships" => "rgb(252, 255, 252)",
_ => "rgb(250, 250, 250)",
}
} else {
match title {
"User Interactions" => "rgb(245, 245, 245)",
"Contract-to-Contract Interactions" => "rgb(240, 248, 255)",
"Event Definitions" => "rgb(255, 245, 245)",
"Contract Relationships" => "rgb(245, 255, 245)",
_ => "rgb(240, 240, 240)",
}
};
diagram.push(format!("rect {}", color));
diagram.push(format!("Note over User: {}", title));
diagram.push("end".to_string());
diagram.push("".to_string());
}
fn add_legend(diagram: &mut Vec<String>, light_colors: bool) {
diagram.push("".to_string());
diagram.push("%%{init: { 'sequence': { 'showSequenceNumbers': true } }}%%".to_string());
diagram.push("".to_string());
let legend_color = if light_colors { "rgb(248, 252, 255)" } else { "rgb(240, 240, 255)" };
diagram.push(format!("rect {}", legend_color));
diagram.push("Note over User: Diagram Legend".to_string());
diagram.push("end".to_string());
diagram.push("".to_string());
diagram.push("Note left of User: User→Contract: Public/External function calls".to_string());
diagram.push("Note left of User: User←Contract: Function returns".to_string());
diagram.push("Note left of User: Contract→Contract: Internal interactions".to_string());
diagram.push("Note left of User: Contract→Events: Emitted events".to_string());
diagram.push(
"Note left of User: Colored sections indicate different interaction types".to_string(),
);
}