Skip to main content

faucet_cli/commands/
new.rs

1//! `faucet new connector` — scaffold a ready-to-build connector crate (#209).
2
3use crate::cli::{NewArgs, NewConnectorArgs, NewTarget};
4use crate::error::{CliError, CliResult};
5use crate::scaffold::{ConnectorKind, ConnectorScaffold};
6
7/// Execute the `new` command.
8pub async fn run(args: NewArgs) -> CliResult<()> {
9    match args.target {
10        NewTarget::Connector(a) => run_connector(a).await,
11    }
12}
13
14/// Scaffold a connector crate.
15async fn run_connector(args: NewConnectorArgs) -> CliResult<()> {
16    let kind = ConnectorKind::parse(&args.kind).map_err(CliError::Config)?;
17    let scaffold =
18        ConnectorScaffold::new(&args.name, kind, args.common).map_err(CliError::Config)?;
19    let files = scaffold.files();
20    let root = &args.output;
21
22    // Fail before writing anything if any target already exists (unless --force).
23    if !args.force {
24        for f in &files {
25            let path = root.join(&f.path);
26            if path.exists() {
27                return Err(CliError::ScaffoldExists { path });
28            }
29        }
30    }
31
32    for f in &files {
33        let path = root.join(&f.path);
34        if let Some(parent) = path.parent() {
35            std::fs::create_dir_all(parent)?;
36        }
37        std::fs::write(&path, &f.contents)?;
38    }
39
40    println!(
41        "Scaffolded {} ({} files):",
42        scaffold.crate_name(),
43        files.len()
44    );
45    for f in &files {
46        println!("  {}", root.join(&f.path).display());
47    }
48    println!(
49        "\nNext:\n  cd {}\n  cargo test          # the generated passthrough compiles & tests green\n  # then implement the TODOs in src/config.rs and src/{}",
50        root.join(scaffold.crate_name()).display(),
51        match kind {
52            ConnectorKind::Source => "stream.rs",
53            ConnectorKind::Sink => "sink.rs",
54        }
55    );
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[tokio::test]
64    async fn scaffolds_a_source_crate_to_disk() {
65        let dir = tempfile::tempdir().unwrap();
66        let args = NewConnectorArgs {
67            name: "acme".into(),
68            kind: "source".into(),
69            common: false,
70            output: dir.path().to_path_buf(),
71            force: false,
72        };
73        run_connector(args).await.expect("scaffold succeeds");
74        let base = dir.path().join("faucet-source-acme");
75        assert!(base.join("Cargo.toml").is_file());
76        assert!(base.join("src/lib.rs").is_file());
77        assert!(base.join("src/config.rs").is_file());
78        assert!(base.join("src/stream.rs").is_file());
79        assert!(base.join("README.md").is_file());
80        let lib = std::fs::read_to_string(base.join("src/lib.rs")).unwrap();
81        assert!(lib.contains("pub use stream::AcmeSource;"));
82    }
83
84    #[tokio::test]
85    async fn refuses_to_overwrite_without_force() {
86        let dir = tempfile::tempdir().unwrap();
87        let mk = || NewConnectorArgs {
88            name: "acme".into(),
89            kind: "sink".into(),
90            common: false,
91            output: dir.path().to_path_buf(),
92            force: false,
93        };
94        run_connector(mk()).await.expect("first scaffold");
95        let err = run_connector(mk()).await.expect_err("second must refuse");
96        assert!(matches!(err, CliError::ScaffoldExists { .. }));
97    }
98
99    #[tokio::test]
100    async fn force_overwrites() {
101        let dir = tempfile::tempdir().unwrap();
102        let mk = |force| NewConnectorArgs {
103            name: "acme".into(),
104            kind: "sink".into(),
105            common: true,
106            output: dir.path().to_path_buf(),
107            force,
108        };
109        run_connector(mk(false)).await.expect("first scaffold");
110        run_connector(mk(true)).await.expect("force overwrite");
111        assert!(dir.path().join("faucet-common-acme/Cargo.toml").is_file());
112    }
113
114    #[tokio::test]
115    async fn rejects_bad_kind() {
116        let dir = tempfile::tempdir().unwrap();
117        let args = NewConnectorArgs {
118            name: "acme".into(),
119            kind: "middleware".into(),
120            common: false,
121            output: dir.path().to_path_buf(),
122            force: false,
123        };
124        assert!(matches!(
125            run_connector(args).await,
126            Err(CliError::Config(_))
127        ));
128    }
129
130    #[tokio::test]
131    async fn rejects_bad_name() {
132        let dir = tempfile::tempdir().unwrap();
133        let args = NewConnectorArgs {
134            name: "Acme_Bad".into(),
135            kind: "source".into(),
136            common: false,
137            output: dir.path().to_path_buf(),
138            force: false,
139        };
140        assert!(matches!(
141            run_connector(args).await,
142            Err(CliError::Config(_))
143        ));
144    }
145}