Bug: [Feature] Add a cargo binary for running diesel migrations locally



### Objective
Add a new binary in the `router` crate that would run migrations locally via cli i.e `cargo run migrations`.

### Context
Currently migrations are managed via the diesel_cli which rely on explicitly providing the database username/password & host,
This is a bit cumbersome when you have to provide the variables over & over again.

```bash
diesel migration --database-url postgres://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME run
```

It would be somewhat easier if we could leverage [`diesel_migrations`](https://docs.rs/diesel_migrations/latest/diesel_migrations/) to read the credentials from config file / env & apply migrations.

(Open to alternative approaches as well for this)

### Outcomes
- A new rust binary that can be used for running local db migrations from file
- Updates to the local setup documentation outlining the usage of this cli tool
diff --git a/Cargo.lock b/Cargo.lock
index 1d6d0fc7511..8249ae4f70b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -384,12 +384,55 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
 [[package]]
 name = "anstyle"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
 
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.81"
@@ -1909,8 +1952,10 @@ version = "4.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
 dependencies = [
+ "anstream",
  "anstyle",
  "clap_lex",
+ "strsim",
 ]
 
 [[package]]
@@ -1940,6 +1985,12 @@ dependencies = [
  "bitflags 1.3.2",
 ]
 
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
 [[package]]
 name = "common_enums"
 version = "0.1.0"
@@ -2016,7 +2067,7 @@ dependencies = [
  "rust-ini",
  "serde",
  "serde_json",
- "toml",
+ "toml 0.8.12",
  "yaml-rust",
 ]
 
@@ -2029,7 +2080,7 @@ dependencies = [
  "indexmap 2.2.6",
  "serde",
  "serde_json",
- "toml",
+ "toml 0.8.12",
 ]
 
 [[package]]
@@ -2039,7 +2090,7 @@ dependencies = [
  "api_models",
  "serde",
  "serde_with",
- "toml",
+ "toml 0.8.12",
  "utoipa",
 ]
 
@@ -2588,9 +2639,9 @@ checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94"
 
 [[package]]
 name = "diesel"
-version = "2.1.5"
+version = "2.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559"
+checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2"
 dependencies = [
  "bitflags 2.5.0",
  "byteorder",
@@ -2604,9 +2655,9 @@ dependencies = [
 
 [[package]]
 name = "diesel_derives"
-version = "2.1.3"
+version = "2.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c"
+checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c"
 dependencies = [
  "diesel_table_macro_syntax",
  "proc-macro2",
@@ -2614,6 +2665,17 @@ dependencies = [
  "syn 2.0.57",
 ]
 
+[[package]]
+name = "diesel_migrations"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
+dependencies = [
+ "diesel",
+ "migrations_internals",
+ "migrations_macros",
+]
+
 [[package]]
 name = "diesel_models"
 version = "0.1.0"
@@ -3584,6 +3646,17 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "hsdev"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "diesel",
+ "diesel_migrations",
+ "serde",
+ "toml 0.5.11",
+]
+
 [[package]]
 name = "http"
 version = "0.2.12"
@@ -4020,6 +4093,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
 [[package]]
 name = "iso_country"
 version = "0.1.4"
@@ -4396,6 +4475,27 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "migrations_internals"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
+dependencies = [
+ "serde",
+ "toml 0.7.8",
+]
+
+[[package]]
+name = "migrations_macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
+dependencies = [
+ "migrations_internals",
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "mimalloc"
 version = "0.1.39"
@@ -7370,7 +7470,7 @@ dependencies = [
  "thirtyfour",
  "time",
  "tokio 1.37.0",
- "toml",
+ "toml 0.8.12",
 ]
 
 [[package]]
@@ -7798,6 +7898,27 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.19.15",
+]
+
 [[package]]
 name = "toml"
 version = "0.8.12"
@@ -7827,6 +7948,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
 dependencies = [
  "indexmap 2.2.6",
+ "serde",
+ "serde_spanned",
  "toml_datetime",
  "winnow 0.5.40",
 ]
@@ -8243,6 +8366,12 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
 [[package]]
 name = "utoipa"
 version = "4.2.0"
diff --git a/crates/hsdev/Cargo.toml b/crates/hsdev/Cargo.toml
new file mode 100644
index 00000000000..6600d41d9fc
--- /dev/null
+++ b/crates/hsdev/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "hsdev"
+version = "0.1.0"
+license.workspace = true 
+edition.workspace = true
+rust-version.workspace = true 
+description = "A simple diesel postgres migrator that uses TOML files"
+repository = "https://github.com/juspay/hyperswitch.git"
+readme = "README.md"
+
+[dependencies]
+diesel = { version = "2.1.6", features = ["postgres"] }
+diesel_migrations = "2.1.0"
+toml = "0.5"
+clap = { version = "4.1.8", features = ["derive"] }
+serde = { version = "1.0", features = ["derive"] }
diff --git a/crates/hsdev/README.md b/crates/hsdev/README.md
new file mode 100644
index 00000000000..926d9b9056f
--- /dev/null
+++ b/crates/hsdev/README.md
@@ -0,0 +1,24 @@
+# HSDEV
+
+`hsdev` is a simple diesel Postgres migration tool. It is designed to simply running a Postgres database migration with diesel.
+
+## Installing hsdev
+`hsdev` can be installed using `cargo`
+```shell
+cargo install --force --path crates/hsdev
+```
+
+## Using hsdev
+Using `hsdev` is simple. All you need to do is run the following command.
+```shell
+hsdev --toml-file [path/to/TOML/file]
+```
+
+provide `hsdev` with a TOML file containing the following keys:
+```toml
+username = "your_username"
+password = "your_password"
+dbname = "your_db_name"
+```
+
+Simply run the command and let `hsdev` handle the rest.
diff --git a/crates/hsdev/src/input_file.rs b/crates/hsdev/src/input_file.rs
new file mode 100644
index 00000000000..0d5d0a2eb0c
--- /dev/null
+++ b/crates/hsdev/src/input_file.rs
@@ -0,0 +1,26 @@
+use std::string::String;
+
+use serde::Deserialize;
+use toml::Value;
+
+#[derive(Deserialize)]
+pub struct InputData {
+    username: String,
+    password: String,
+    dbname: String,
+    host: String,
+    port: u16,
+}
+
+impl InputData {
+    pub fn read(db_table: &Value) -> Result<Self, toml::de::Error> {
+        db_table.clone().try_into()
+    }
+
+    pub fn postgres_url(&self) -> String {
+        format!(
+            "postgres://{}:{}@{}:{}/{}",
+            self.username, self.password, self.host, self.port, self.dbname
+        )
+    }
+}
diff --git a/crates/hsdev/src/main.rs b/crates/hsdev/src/main.rs
new file mode 100644
index 00000000000..e6ee9e17d44
--- /dev/null
+++ b/crates/hsdev/src/main.rs
@@ -0,0 +1,139 @@
+use clap::{Parser, ValueHint};
+use diesel::{pg::PgConnection, Connection};
+use diesel_migrations::{FileBasedMigrations, HarnessWithOutput, MigrationHarness};
+use toml::Value;
+
+mod input_file;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    #[arg(short, long, value_hint = ValueHint::FilePath)]
+    toml_file: std::path::PathBuf,
+
+    #[arg(long, default_value_t = String::from(""))]
+    toml_table: String,
+}
+
+fn main() {
+    let args = Args::parse();
+
+    let toml_file = &args.toml_file;
+    let table_name = &args.toml_table;
+    let toml_contents = match std::fs::read_to_string(toml_file) {
+        Ok(contents) => contents,
+        Err(e) => {
+            eprintln!("Error reading TOML file: {}", e);
+            return;
+        }
+    };
+    let toml_data: Value = match toml_contents.parse() {
+        Ok(data) => data,
+        Err(e) => {
+            eprintln!("Error parsing TOML file: {}", e);
+            return;
+        }
+    };
+
+    let table = get_toml_table(table_name, &toml_data);
+
+    let input = match input_file::InputData::read(table) {
+        Ok(data) => data,
+        Err(e) => {
+            eprintln!("Error loading TOML file: {}", e);
+            return;
+        }
+    };
+
+    let db_url = input.postgres_url();
+
+    println!("Attempting to connect to {}", db_url);
+
+    let mut conn = match PgConnection::establish(&db_url) {
+        Ok(value) => value,
+        Err(_) => {
+            eprintln!("Unable to establish database connection");
+            return;
+        }
+    };
+
+    let migrations = match FileBasedMigrations::find_migrations_directory() {
+        Ok(value) => value,
+        Err(_) => {
+            eprintln!("Could not find migrations directory");
+            return;
+        }
+    };
+
+    let mut harness = HarnessWithOutput::write_to_stdout(&mut conn);
+
+    match harness.run_pending_migrations(migrations) {
+        Ok(_) => println!("Successfully ran migrations"),
+        Err(_) => eprintln!("Couldn't run migrations"),
+    };
+}
+
+pub fn get_toml_table<'a>(table_name: &'a str, toml_data: &'a Value) -> &'a Value {
+    if !table_name.is_empty() {
+        match toml_data.get(table_name) {
+            Some(value) => value,
+            None => {
+                eprintln!("Unable to find toml table: \"{}\"", &table_name);
+                std::process::abort()
+            }
+        }
+    } else {
+        toml_data
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #![allow(clippy::unwrap_used)]
+
+    use std::str::FromStr;
+
+    use toml::Value;
+
+    use crate::{get_toml_table, input_file::InputData};
+
+    #[test]
+    fn test_input_file() {
+        let toml_str = r#"username = "db_user"
+            password = "db_pass"
+            dbname = "db_name"
+            host = "localhost"
+            port = 5432"#;
+
+        let toml_value = Value::from_str(toml_str);
+        assert!(toml_value.is_ok());
+        let toml_value = toml_value.unwrap();
+
+        let toml_table = InputData::read(&toml_value);
+        assert!(toml_table.is_ok());
+        let toml_table = toml_table.unwrap();
+
+        let db_url = toml_table.postgres_url();
+        assert_eq!("postgres://db_user:db_pass@localhost:5432/db_name", db_url);
+    }
+
+    #[test]
+    fn test_given_toml() {
+        let toml_str_table = r#"[database]
+            username = "db_user"
+            password = "db_pass"
+            dbname = "db_name"
+            host = "localhost"
+            port = 5432"#;
+
+        let table_name = "database";
+        let toml_value = Value::from_str(toml_str_table).unwrap();
+        let table = get_toml_table(table_name, &toml_value);
+
+        assert!(table.is_table());
+
+        let table_name = "";
+        let table = get_toml_table(table_name, &toml_value);
+        assert!(table.is_table());
+    }
+}

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

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

## Description
This PR closes #4589 
Created a new Rust binary in the `crates` directory which automates the Diesel migrations with Postgres. The new tool is a CLI tool that takes a path to a TOML file as an argument along with the name of a TOML table to extract Postgres database credentials from. The tool then automatically performs the migrations by utilizing `diesel_migrations` library. From our testing, we were able to set up Hyperswitch locally without using `diesel_cli` and Bash variables on WSL2 Ubuntu.

I worked on this Issue with @JamesM25 @ind1goDusk

The example below shows the application running using `cargo run` in the `hsdev` crate.
```sh
cargo run -- --toml-file ~/hyperswitch/config/development.toml --toml-table master_database
```

### 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
Before our tool, Hyperswitch needed to be set up on local machines using a long `diesel_cli` command with Bash variables for the Postgres migration. Now we have provided a simpler and easily repeatable solution to Postgres and Diesel migrations that allows for greater modularity as you can select specific TOML files and tables to select database credentials from.


## How did you test it?
We wrote two units test on the core functionality. We tested it both on a dummy TOML file as well as in Hyperswitch repository. We were able to set up Hyperswitch locally using `hsdev` instead of `diesel_cli`.

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

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