faucet_cli/commands/
new.rs1use crate::cli::{NewArgs, NewConnectorArgs, NewTarget};
4use crate::error::{CliError, CliResult};
5use crate::scaffold::{ConnectorKind, ConnectorScaffold};
6
7pub async fn run(args: NewArgs) -> CliResult<()> {
9 match args.target {
10 NewTarget::Connector(a) => run_connector(a).await,
11 }
12}
13
14async 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 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}