faucet_cli/commands/
validate.rs1use crate::cli::ValidateArgs;
7use crate::config::PipelineConfig;
8use crate::error::{CliError, CliResult};
9use crate::expand::{NodeRole, expand};
10use crate::registry::{sink_schema, source_schema};
11use crate::state::available_state_kinds;
12use crate::transforms::available_transforms;
13
14pub async fn run(args: ValidateArgs) -> CliResult<()> {
16 let cwd = std::env::current_dir()?;
17 let env_path =
18 crate::env_loader::resolve_env_file(args.env_file.as_deref(), args.no_env_file, &cwd)?;
19 crate::env_loader::load_env_file_if_present(env_path.as_deref())?;
20
21 let path = match args.config {
22 Some(p) => p,
23 None => crate::env_loader::discover_config_path(&cwd).ok_or(CliError::NoConfigOrFromEnv)?,
24 };
25 let cfg = if args.no_secrets {
26 PipelineConfig::from_path_tolerating_secrets(&path)?
28 } else {
29 let refs = crate::secrets::scan_path_refs(&path)?;
31 let cfg = PipelineConfig::from_path_async(&path).await?;
32 for (scheme, reference) in &refs {
33 println!("secret: {scheme}:{reference} → resolved");
34 }
35 cfg
36 };
37 let nodes = expand(&cfg)?;
38
39 #[cfg(feature = "schedule")]
42 if let Some(spec) = &cfg.schedule {
43 crate::schedule::compiled::CompiledSchedule::compile(spec)?;
44 println!(
45 "schedule: cron '{}' tz '{}' — valid",
46 spec.cron, spec.timezone
47 );
48 }
49
50 for node in &nodes {
51 source_schema(&node.source.kind)?;
53 sink_schema(&node.sink.kind)?;
54 for t in &node.transforms {
55 if !available_transforms().contains(&t.kind.as_str()) {
56 return Err(CliError::UnknownTransform {
57 name: format!("{} (row '{}')", t.kind, node.id),
58 available: available_transforms().join(", "),
59 });
60 }
61 }
62 if let Some(state) = &node.state
63 && !available_state_kinds().contains(&state.kind.as_str())
64 {
65 return Err(CliError::UnknownStateStore {
66 name: format!("{} (row '{}')", state.kind, node.id),
67 available: available_state_kinds().join(", "),
68 });
69 }
70 }
71
72 let roots = nodes
73 .iter()
74 .filter(|n| matches!(n.role, NodeRole::Root))
75 .count();
76 let children = nodes.len() - roots;
77 println!(
78 "ok: '{}' rows={} (roots={}, children={}) execution={}",
79 cfg.name.as_deref().unwrap_or("(unnamed)"),
80 nodes.len(),
81 roots,
82 children,
83 cfg.execution
84 .as_ref()
85 .map(|e| format!(
86 "max_concurrent={:?} on_error={:?}",
87 e.max_concurrent.unwrap_or(0),
88 e.on_error
89 ))
90 .unwrap_or_else(|| "(defaults)".to_owned()),
91 );
92 for node in &nodes {
93 let role = match &node.role {
94 NodeRole::Root => "root".to_owned(),
95 NodeRole::Child {
96 parent_id,
97 parent_key,
98 } => {
99 format!("child of '{parent_id}' (parent_key={parent_key})")
100 }
101 };
102 println!(
103 " - {} [{}] source={} sink={}",
104 node.id, role, node.source.kind, node.sink.kind
105 );
106 }
107 Ok(())
108}