feat(constraint_graph): add visualization functionality to the constraint graph (#4701)

This commit is contained in:
Shanks
2024-05-21 17:28:52 +05:30
committed by GitHub
parent 2e79ee0615
commit 0f53f74d26
9 changed files with 197 additions and 3 deletions

54
Cargo.lock generated
View File

@ -2618,6 +2618,21 @@ dependencies = [
"const-random", "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]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@ -3253,6 +3268,22 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "h2" name = "h2"
version = "0.3.25" version = "0.3.25"
@ -3624,6 +3655,7 @@ name = "hyperswitch_constraint_graph"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"erased-serde 0.3.31", "erased-serde 0.3.31",
"graphviz-rust",
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_json", "serde_json",
@ -3774,6 +3806,28 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.4" version = "0.1.4"

View File

@ -21,7 +21,7 @@ utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order
# First party dependencies # First party dependencies
common_enums = { version = "0.1.0", path = "../common_enums" } 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" } euclid_macros = { version = "0.1.0", path = "../euclid_macros" }
[features] [features]

View File

@ -22,6 +22,12 @@ pub mod euclid_graph_prelude {
impl cgraph::KeyNode for dir::DirKey {} 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 { impl cgraph::ValueNode for dir::DirValue {
type Key = dir::DirKey; 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)] #[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type", content = "details", rename_all = "snake_case")] #[serde(tag = "type", content = "details", rename_all = "snake_case")]
pub enum AnalysisError<V: cgraph::ValueNode> { pub enum AnalysisError<V: cgraph::ValueNode> {

View File

@ -289,7 +289,6 @@ pub enum DirKeyKind {
#[serde(rename = "billing_country")] #[serde(rename = "billing_country")]
BillingCountry, BillingCountry,
#[serde(skip_deserializing, rename = "connector")] #[serde(skip_deserializing, rename = "connector")]
#[strum(disabled)]
Connector, Connector,
#[strum( #[strum(
serialize = "business_label", serialize = "business_label",

View File

@ -7,8 +7,12 @@ rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
viz = ["dep:graphviz-rust"]
[dependencies] [dependencies]
erased-serde = "0.3.28" erased-serde = "0.3.28"
graphviz-rust = { version = "0.6.2", optional = true }
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
serde = { version = "1.0.163", features = ["derive", "rc"] } serde = { version = "1.0.163", features = ["derive", "rc"] }
serde_json = "1.0.96" serde_json = "1.0.96"

View File

@ -585,3 +585,92 @@ where
Ok(node_builder.build()) 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,
<V as ValueNode>::Key: NodeViz,
{
fn get_node_label(node: &types::Node<V>) -> 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::<Vec<_>>();
format!("{key} in [{}]", nodes.join(", "))
}
};
format!("\"{label}\"")
}
fn build_node(cg_node_id: types::NodeId, cg_node: &types::Node<V>) -> 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::<Vec<_>>()
)
}
pub fn get_viz_digraph_string(&self) -> String {
let mut ctx = PrinterContext::default();
let digraph = self.get_viz_digraph();
digraph.print(&mut ctx)
}
}
}

View File

@ -7,6 +7,8 @@ pub mod types;
pub use builder::ConstraintGraphBuilder; pub use builder::ConstraintGraphBuilder;
pub use error::{AnalysisTrace, GraphError}; pub use error::{AnalysisTrace, GraphError};
pub use graph::ConstraintGraph; pub use graph::ConstraintGraph;
#[cfg(feature = "viz")]
pub use types::NodeViz;
pub use types::{ pub use types::{
CheckingContext, CycleCheck, DomainId, DomainIdentifier, Edge, EdgeId, KeyNode, Memoization, CheckingContext, CycleCheck, DomainId, DomainIdentifier, Edge, EdgeId, KeyNode, Memoization,
Node, NodeId, NodeValue, Relation, Strength, ValueNode, Node, NodeId, NodeValue, Relation, Strength, ValueNode,

View File

@ -17,6 +17,11 @@ pub trait ValueNode: fmt::Debug + Clone + hash::Hash + serde::Serialize + Partia
fn get_key(&self) -> Self::Key; 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)] #[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq, Hash)]
#[serde(transparent)] #[serde(transparent)]
pub struct NodeId(usize); pub struct NodeId(usize);

View File

@ -13,7 +13,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect
[dependencies] [dependencies]
api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } api_models = { version = "0.1.0", path = "../api_models", package = "api_models" }
common_enums = { version = "0.1.0", path = "../common_enums" } 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" } euclid = { version = "0.1.0", path = "../euclid" }
masking = { version = "0.1.0", path = "../masking/" } masking = { version = "0.1.0", path = "../masking/" }