Skip to main content

diff_as_lib/
diff_as_lib.rs

1//! # Diffly — library usage example
2//!
3//! Shows three common patterns for consuming Diffly as a Rust library:
4//!
5//! 1. **From a config file** — simplest, mirrors the CLI
6//! 2. **Programmatic config** — build `AppConfig` in code, no TOML file needed
7//! 3. **Inspect the changeset** — traverse the diff result for custom logic
8//!
9//! Run with a config file:
10//!   cargo run --example diff_as_lib -- diffly.toml
11//!
12//! Run with the built-in programmatic config (needs a local PostgreSQL):
13//!   cargo run --example diff_as_lib
14
15use std::collections::BTreeMap;
16
17use anyhow::Result;
18use diffly::{
19    presentation::writers::{all_writers, write_to_file, writer_for},
20    AppConfig, Changeset, DbConfig, DiffConfig, ExcludedColumns, OutputConfig, TableConfig,
21};
22
23#[tokio::main]
24async fn main() -> Result<()> {
25    let args: Vec<String> = std::env::args().collect();
26
27    match args.get(1).map(String::as_str) {
28        Some(path) => from_config_file(path).await,
29        None => programmatic_config().await,
30    }
31}
32
33// ─────────────────────────────────────────────────────────────────────────────
34// Pattern 1 — load config from a TOML file (same as the CLI does internally)
35// ─────────────────────────────────────────────────────────────────────────────
36async fn from_config_file(path: &str) -> Result<()> {
37    println!("=== Pattern 1: from config file ({path}) ===\n");
38
39    let cfg = AppConfig::load(path)?;
40    let changeset = diffly::run(&cfg).await?;
41
42    // Write all three output formats (JSON / SQL / HTML)
43    for writer in all_writers() {
44        write_to_file(&*writer, &changeset, &cfg.output.dir)?;
45        println!(
46            "Written: {}/{}.{}",
47            cfg.output.dir,
48            changeset.changeset_id,
49            writer.extension()
50        );
51    }
52
53    print_summary(&changeset);
54    Ok(())
55}
56
57// ─────────────────────────────────────────────────────────────────────────────
58// Pattern 2 — build AppConfig entirely in code, no TOML file required.
59// Useful when config comes from env vars, a CLI flag, a database row, etc.
60// ─────────────────────────────────────────────────────────────────────────────
61async fn programmatic_config() -> Result<()> {
62    println!("=== Pattern 2: programmatic config ===\n");
63
64    let db = |schema: &str| DbConfig {
65        driver: "postgres".into(),
66        host: std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".into()),
67        port: 5432,
68        dbname: "diffly".into(),
69        user: "diffly".into(),
70        password: "diffly".into(),
71        schema: schema.into(),
72    };
73
74    let cfg = AppConfig {
75        source: db("source"),
76        target: db("target"),
77        diff: DiffConfig {
78            tables: vec![
79                TableConfig {
80                    name: "pricing_rules".into(),
81                    primary_key: vec!["id".into()],
82                    excluded_columns: ExcludedColumns(vec![
83                        "created_at".into(),
84                        "updated_at".into(),
85                    ]),
86                },
87                TableConfig {
88                    name: "discount_tiers".into(),
89                    primary_key: vec!["id".into()],
90                    excluded_columns: ExcludedColumns::default(),
91                },
92                TableConfig {
93                    name: "tax_rules".into(),
94                    primary_key: vec!["region_code".into(), "product_category".into()],
95                    excluded_columns: ExcludedColumns::default(),
96                },
97            ],
98        },
99        output: OutputConfig {
100            dir: "./output".into(),
101        },
102    };
103
104    let changeset = diffly::run(&cfg).await?;
105
106    // Write only the SQL migration file
107    let sql_writer = writer_for("sql").expect("sql writer always available");
108    write_to_file(&*sql_writer, &changeset, &cfg.output.dir)?;
109    println!(
110        "SQL written: {}/{}.sql\n",
111        cfg.output.dir, changeset.changeset_id
112    );
113
114    // Hand off to pattern 3
115    inspect_changeset(&changeset);
116    Ok(())
117}
118
119// ─────────────────────────────────────────────────────────────────────────────
120// Pattern 3 — inspect the Changeset directly for custom logic.
121// The Changeset is plain serialisable Rust data — no magic, no callbacks.
122// ─────────────────────────────────────────────────────────────────────────────
123fn inspect_changeset(changeset: &Changeset) {
124    println!("=== Pattern 3: inspecting the changeset ===\n");
125    println!("id      : {}", changeset.changeset_id);
126    println!("source  : {}", changeset.source_schema);
127    println!("target  : {}", changeset.target_schema);
128    println!("driver  : {}", changeset.driver);
129    println!();
130
131    for table in &changeset.tables {
132        if table.is_empty() {
133            continue;
134        }
135
136        println!("━━ {} ━━", table.table_name);
137
138        for ins in &table.inserts {
139            println!("  + INSERT  {}", fmt_pk(&ins.pk));
140        }
141
142        for upd in &table.updates {
143            let pk = fmt_pk(&upd.pk);
144            for col in &upd.changed_columns {
145                println!(
146                    "  ~ UPDATE  {}  {}: {} → {}",
147                    pk, col.column, col.before, col.after
148                );
149            }
150        }
151
152        for del in &table.deletes {
153            println!("  - DELETE  {}", fmt_pk(&del.pk));
154        }
155
156        println!();
157    }
158
159    // Example: abort a deployment pipeline when unexpected deletes are detected
160    if changeset.summary.total_deletes > 0 {
161        eprintln!(
162            "⚠  {} delete(s) detected — review before applying to production.",
163            changeset.summary.total_deletes,
164        );
165    }
166
167    // Example: serialise to JSON and send to a webhook / write to a log
168    let json = serde_json::to_string_pretty(changeset).expect("Changeset is always serialisable");
169    println!("Full changeset: {} bytes of JSON", json.len());
170
171    print_summary(changeset);
172}
173
174fn fmt_pk(pk: &BTreeMap<String, serde_json::Value>) -> String {
175    pk.iter()
176        .map(|(k, v)| format!("{k}={v}"))
177        .collect::<Vec<_>>()
178        .join(", ")
179}
180
181fn print_summary(changeset: &Changeset) {
182    println!("\n── summary ──────────────────────");
183    println!("  inserts : {}", changeset.summary.total_inserts);
184    println!("  updates : {}", changeset.summary.total_updates);
185    println!("  deletes : {}", changeset.summary.total_deletes);
186    println!("  tables  : {}", changeset.summary.tables_affected);
187}