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 let cap = match kind {
59 ConnectorKind::Source => "native streaming (override stream_pages) + resumable bookmarks",
60 ConnectorKind::Sink => "idempotent writes + upsert / schema evolution",
61 };
62 println!(
63 "\nConformance tier: ⚪ Draft → reach 🟢 Stable by\n \
64 [ ] adding a verified entry to cli/connectors/registry.json\n \
65 [ ] a complete config_schema() (already scaffolded)\n \
66 [ ] a one-line description in the connector catalog\n \
67 [ ] {cap}\n \
68 [ ] unit + integration tests (run the faucet-conformance battery)\n\
69 Check your score any time with `faucet conformance {}`.",
70 scaffold.crate_name(),
71 );
72 Ok(())
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[tokio::test]
80 async fn scaffolds_a_source_crate_to_disk() {
81 let dir = tempfile::tempdir().unwrap();
82 let args = NewConnectorArgs {
83 name: "acme".into(),
84 kind: "source".into(),
85 common: false,
86 output: dir.path().to_path_buf(),
87 force: false,
88 };
89 run_connector(args).await.expect("scaffold succeeds");
90 let base = dir.path().join("faucet-source-acme");
91 assert!(base.join("Cargo.toml").is_file());
92 assert!(base.join("src/lib.rs").is_file());
93 assert!(base.join("src/config.rs").is_file());
94 assert!(base.join("src/stream.rs").is_file());
95 assert!(base.join("README.md").is_file());
96 let lib = std::fs::read_to_string(base.join("src/lib.rs")).unwrap();
97 assert!(lib.contains("pub use stream::AcmeSource;"));
98 }
99
100 #[tokio::test]
101 async fn refuses_to_overwrite_without_force() {
102 let dir = tempfile::tempdir().unwrap();
103 let mk = || NewConnectorArgs {
104 name: "acme".into(),
105 kind: "sink".into(),
106 common: false,
107 output: dir.path().to_path_buf(),
108 force: false,
109 };
110 run_connector(mk()).await.expect("first scaffold");
111 let err = run_connector(mk()).await.expect_err("second must refuse");
112 assert!(matches!(err, CliError::ScaffoldExists { .. }));
113 }
114
115 #[tokio::test]
116 async fn force_overwrites() {
117 let dir = tempfile::tempdir().unwrap();
118 let mk = |force| NewConnectorArgs {
119 name: "acme".into(),
120 kind: "sink".into(),
121 common: true,
122 output: dir.path().to_path_buf(),
123 force,
124 };
125 run_connector(mk(false)).await.expect("first scaffold");
126 run_connector(mk(true)).await.expect("force overwrite");
127 assert!(dir.path().join("faucet-common-acme/Cargo.toml").is_file());
128 }
129
130 #[tokio::test]
131 async fn rejects_bad_kind() {
132 let dir = tempfile::tempdir().unwrap();
133 let args = NewConnectorArgs {
134 name: "acme".into(),
135 kind: "middleware".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
146 #[tokio::test]
147 async fn rejects_bad_name() {
148 let dir = tempfile::tempdir().unwrap();
149 let args = NewConnectorArgs {
150 name: "Acme_Bad".into(),
151 kind: "source".into(),
152 common: false,
153 output: dir.path().to_path_buf(),
154 force: false,
155 };
156 assert!(matches!(
157 run_connector(args).await,
158 Err(CliError::Config(_))
159 ));
160 }
161}