From 0f53f74d26e829602519998c41a460dc9a4809af Mon Sep 17 00:00:00 2001 From: Shanks Date: Tue, 21 May 2024 17:28:52 +0530 Subject: [PATCH] feat(constraint_graph): add visualization functionality to the constraint graph (#4701) --- Cargo.lock | 54 +++++++++++ crates/euclid/Cargo.toml | 2 +- crates/euclid/src/dssa/graph.rs | 41 +++++++++ crates/euclid/src/frontend/dir.rs | 1 - .../hyperswitch_constraint_graph/Cargo.toml | 4 + .../hyperswitch_constraint_graph/src/graph.rs | 89 +++++++++++++++++++ .../hyperswitch_constraint_graph/src/lib.rs | 2 + .../hyperswitch_constraint_graph/src/types.rs | 5 ++ crates/kgraph_utils/Cargo.toml | 2 +- 9 files changed, 197 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5dc4e2ee9..8a9e036983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2618,6 +2618,21 @@ dependencies = [ "const-random", ] +[[package]] +name = "dot-generator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aaac7ada45f71873ebce336491d1c1bc4a7c8042c7cea978168ad59e805b871" +dependencies = [ + "dot-structures", +] + +[[package]] +name = "dot-structures" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675e35c02a51bb4d4618cb4885b3839ce6d1787c97b664474d9208d074742e20" + [[package]] name = "dotenvy" version = "0.15.7" @@ -3253,6 +3268,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "graphviz-rust" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27dafd1ac303e0dfb347a3861d9ac440859bab26ec2f534bbceb262ea492a1e0" +dependencies = [ + "dot-generator", + "dot-structures", + "into-attr", + "into-attr-derive", + "pest", + "pest_derive", + "rand", + "tempfile", +] + [[package]] name = "h2" version = "0.3.25" @@ -3624,6 +3655,7 @@ name = "hyperswitch_constraint_graph" version = "0.1.0" dependencies = [ "erased-serde 0.3.31", + "graphviz-rust", "rustc-hash", "serde", "serde_json", @@ -3774,6 +3806,28 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "into-attr" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b48c537e49a709e678caec3753a7dba6854661a1eaa27675024283b3f8b376" +dependencies = [ + "dot-structures", +] + +[[package]] +name = "into-attr-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecac7c1ae6cd2c6a3a64d1061a8bdc7f52ff62c26a831a2301e54c1b5d70d5b1" +dependencies = [ + "dot-generator", + "dot-structures", + "into-attr", + "quote", + "syn 1.0.109", +] + [[package]] name = "iovec" version = "0.1.4" diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index 3341746ab7..b408c01357 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -21,7 +21,7 @@ utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order # First party dependencies common_enums = { version = "0.1.0", path = "../common_enums" } -hyperswitch_constraint_graph = { version = "0.1.0", path = "../hyperswitch_constraint_graph" } +hyperswitch_constraint_graph = { version = "0.1.0", path = "../hyperswitch_constraint_graph", features = ["viz"] } euclid_macros = { version = "0.1.0", path = "../euclid_macros" } [features] diff --git a/crates/euclid/src/dssa/graph.rs b/crates/euclid/src/dssa/graph.rs index 0ffafe4d48..1c476ee81b 100644 --- a/crates/euclid/src/dssa/graph.rs +++ b/crates/euclid/src/dssa/graph.rs @@ -22,6 +22,12 @@ pub mod euclid_graph_prelude { impl cgraph::KeyNode for dir::DirKey {} +impl cgraph::NodeViz for dir::DirKey { + fn viz(&self) -> String { + self.kind.to_string() + } +} + impl cgraph::ValueNode for dir::DirValue { type Key = dir::DirKey; @@ -30,6 +36,41 @@ impl cgraph::ValueNode for dir::DirValue { } } +impl cgraph::NodeViz for dir::DirValue { + fn viz(&self) -> String { + match self { + Self::PaymentMethod(pm) => pm.to_string(), + Self::CardBin(bin) => bin.value.clone(), + Self::CardType(ct) => ct.to_string(), + Self::CardNetwork(cn) => cn.to_string(), + Self::PayLaterType(plt) => plt.to_string(), + Self::WalletType(wt) => wt.to_string(), + Self::UpiType(ut) => ut.to_string(), + Self::BankTransferType(btt) => btt.to_string(), + Self::BankRedirectType(brt) => brt.to_string(), + Self::BankDebitType(bdt) => bdt.to_string(), + Self::CryptoType(ct) => ct.to_string(), + Self::RewardType(rt) => rt.to_string(), + Self::PaymentAmount(amt) => amt.number.to_string(), + Self::PaymentCurrency(curr) => curr.to_string(), + Self::AuthenticationType(at) => at.to_string(), + Self::CaptureMethod(cm) => cm.to_string(), + Self::BusinessCountry(bc) => bc.to_string(), + Self::BillingCountry(bc) => bc.to_string(), + Self::Connector(conn) => conn.connector.to_string(), + Self::MetaData(mv) => format!("[{} = {}]", mv.key, mv.value), + Self::MandateAcceptanceType(mat) => mat.to_string(), + Self::MandateType(mt) => mt.to_string(), + Self::PaymentType(pt) => pt.to_string(), + Self::VoucherType(vt) => vt.to_string(), + Self::GiftCardType(gct) => gct.to_string(), + Self::BusinessLabel(bl) => bl.value.to_string(), + Self::SetupFutureUsage(sfu) => sfu.to_string(), + Self::CardRedirectType(crt) => crt.to_string(), + } + } +} + #[derive(Debug, Clone, serde::Serialize)] #[serde(tag = "type", content = "details", rename_all = "snake_case")] pub enum AnalysisError { diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index 455330fcf7..bc16057fbb 100644 --- a/crates/euclid/src/frontend/dir.rs +++ b/crates/euclid/src/frontend/dir.rs @@ -289,7 +289,6 @@ pub enum DirKeyKind { #[serde(rename = "billing_country")] BillingCountry, #[serde(skip_deserializing, rename = "connector")] - #[strum(disabled)] Connector, #[strum( serialize = "business_label", diff --git a/crates/hyperswitch_constraint_graph/Cargo.toml b/crates/hyperswitch_constraint_graph/Cargo.toml index 425855a05b..4fe97b2b1a 100644 --- a/crates/hyperswitch_constraint_graph/Cargo.toml +++ b/crates/hyperswitch_constraint_graph/Cargo.toml @@ -7,8 +7,12 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +viz = ["dep:graphviz-rust"] + [dependencies] erased-serde = "0.3.28" +graphviz-rust = { version = "0.6.2", optional = true } rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive", "rc"] } serde_json = "1.0.96" diff --git a/crates/hyperswitch_constraint_graph/src/graph.rs b/crates/hyperswitch_constraint_graph/src/graph.rs index d0a98e1952..6aed505728 100644 --- a/crates/hyperswitch_constraint_graph/src/graph.rs +++ b/crates/hyperswitch_constraint_graph/src/graph.rs @@ -585,3 +585,92 @@ where Ok(node_builder.build()) } } + +#[cfg(feature = "viz")] +mod viz { + use graphviz_rust::{ + dot_generator::*, + dot_structures::*, + printer::{DotPrinter, PrinterContext}, + }; + + use crate::{dense_map::EntityId, types, ConstraintGraph, NodeViz, ValueNode}; + + fn get_node_id(node_id: types::NodeId) -> String { + format!("N{}", node_id.get_id()) + } + + impl<'a, V> ConstraintGraph<'a, V> + where + V: ValueNode + NodeViz, + ::Key: NodeViz, + { + fn get_node_label(node: &types::Node) -> String { + let label = match &node.node_type { + types::NodeType::Value(types::NodeValue::Key(key)) => format!("any {}", key.viz()), + types::NodeType::Value(types::NodeValue::Value(val)) => { + format!("{} = {}", val.get_key().viz(), val.viz()) + } + types::NodeType::AllAggregator => "&&".to_string(), + types::NodeType::AnyAggregator => "| |".to_string(), + types::NodeType::InAggregator(agg) => { + let key = if let Some(val) = agg.iter().next() { + val.get_key().viz() + } else { + return "empty in".to_string(); + }; + + let nodes = agg.iter().map(NodeViz::viz).collect::>(); + format!("{key} in [{}]", nodes.join(", ")) + } + }; + + format!("\"{label}\"") + } + + fn build_node(cg_node_id: types::NodeId, cg_node: &types::Node) -> Node { + let viz_node_id = get_node_id(cg_node_id); + let viz_node_label = Self::get_node_label(cg_node); + + node!(viz_node_id; attr!("label", viz_node_label)) + } + + fn build_edge(cg_edge: &types::Edge) -> Edge { + let pred_vertex = get_node_id(cg_edge.pred); + let succ_vertex = get_node_id(cg_edge.succ); + let arrowhead = match cg_edge.strength { + types::Strength::Weak => "onormal", + types::Strength::Normal => "normal", + types::Strength::Strong => "normalnormal", + }; + let color = match cg_edge.relation { + types::Relation::Positive => "blue", + types::Relation::Negative => "red", + }; + + edge!( + node_id!(pred_vertex) => node_id!(succ_vertex); + attr!("arrowhead", arrowhead), + attr!("color", color) + ) + } + + pub fn get_viz_digraph(&self) -> Graph { + graph!( + strict di id!("constraint_graph"), + self.nodes + .iter() + .map(|(node_id, node)| Self::build_node(node_id, node)) + .map(Stmt::Node) + .chain(self.edges.values().map(Self::build_edge).map(Stmt::Edge)) + .collect::>() + ) + } + + pub fn get_viz_digraph_string(&self) -> String { + let mut ctx = PrinterContext::default(); + let digraph = self.get_viz_digraph(); + digraph.print(&mut ctx) + } + } +} diff --git a/crates/hyperswitch_constraint_graph/src/lib.rs b/crates/hyperswitch_constraint_graph/src/lib.rs index ade9a64272..6877169732 100644 --- a/crates/hyperswitch_constraint_graph/src/lib.rs +++ b/crates/hyperswitch_constraint_graph/src/lib.rs @@ -7,6 +7,8 @@ pub mod types; pub use builder::ConstraintGraphBuilder; pub use error::{AnalysisTrace, GraphError}; pub use graph::ConstraintGraph; +#[cfg(feature = "viz")] +pub use types::NodeViz; pub use types::{ CheckingContext, CycleCheck, DomainId, DomainIdentifier, Edge, EdgeId, KeyNode, Memoization, Node, NodeId, NodeValue, Relation, Strength, ValueNode, diff --git a/crates/hyperswitch_constraint_graph/src/types.rs b/crates/hyperswitch_constraint_graph/src/types.rs index d1d14bd7e5..51818f2fee 100644 --- a/crates/hyperswitch_constraint_graph/src/types.rs +++ b/crates/hyperswitch_constraint_graph/src/types.rs @@ -17,6 +17,11 @@ pub trait ValueNode: fmt::Debug + Clone + hash::Hash + serde::Serialize + Partia fn get_key(&self) -> Self::Key; } +#[cfg(feature = "viz")] +pub trait NodeViz { + fn viz(&self) -> String; +} + #[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq, Hash)] #[serde(transparent)] pub struct NodeId(usize); diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index 86de6002c3..2f570fcac5 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -13,7 +13,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } common_enums = { version = "0.1.0", path = "../common_enums" } -hyperswitch_constraint_graph = { version = "0.1.0", path = "../hyperswitch_constraint_graph" } +hyperswitch_constraint_graph = { version = "0.1.0", path = "../hyperswitch_constraint_graph", features = ["viz"] } euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking/" }