Bug: [FEATURE] Constraint Graph visualization (via GraphViz) functionality



### Feature Description

It would be good for developers to be able to visualize constraint graphs for intuition and debugging purposes. This can be done by allowing the constraint graph to describe its structure in the form of a DOT program, which can then be visualized using GraphViz.


### Possible Implementation

The idea is to use a Rust crate that allows us to take any constraint graph and convert its edges and nodes into a GraphViz Digraph in the DOT DSL which can then be used to generate a diagram using any DOT-compatible CLI/UI tool.

### Have you spent some time checking if this feature request has been raised before?

- [X] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/Cargo.lock b/Cargo.lock
index abc265ad692..3f9bd9a6303 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2617,6 +2617,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"
@@ -3252,6 +3267,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"
@@ -3623,6 +3654,7 @@ name = "hyperswitch_constraint_graph"
 version = "0.1.0"
 dependencies = [
  "erased-serde 0.3.31",
+ "graphviz-rust",
  "rustc-hash",
  "serde",
  "serde_json",
@@ -3773,6 +3805,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 3341746ab74..b408c013572 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 0ffafe4d48b..1c476ee81b2 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<V: cgraph::ValueNode> {
diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs
index 455330fcf76..bc16057fbb5 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 425855a05b0..4fe97b2b1ac 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 d0a98e19520..6aed505728e 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,
+        <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)
+        }
+    }
+}
diff --git a/crates/hyperswitch_constraint_graph/src/lib.rs b/crates/hyperswitch_constraint_graph/src/lib.rs
index ade9a64272f..6877169732c 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 d1d14bd7e5c..51818f2fee5 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 86de6002c32..2f570fcac57 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/" }
 

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
This PR adds functionality to the constraint graph that allows the graph to describe its structure by generating a DOT program, which can then be visualized by the user through GraphViz.

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Being able to visualize a constraint graph would aid with intuition and debugging.

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
compiler guided. No API tests required since this is purely a developer-focused feature and does not touch any business logic code.

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
