Skip to main content

faucet_cli/commands/
schema.rs

1//! `faucet schema` — print the JSON Schema for a connector's config.
2
3use crate::cli::{SchemaArgs, SchemaTarget};
4use crate::error::{CliError, CliResult};
5use crate::registry::{sink_schema, source_schema};
6use crate::transforms::transform_schema;
7
8/// Every valid `faucet schema <target>` keyword compiled into this binary, in a
9/// stable order. Feature-gated targets appear only when their feature is on, so
10/// the listing matches what this build can actually emit. `source`, `sink`, and
11/// `transform` additionally take a connector/transform NAME argument.
12pub fn schema_targets() -> Vec<&'static str> {
13    let mut targets = vec![
14        "config",
15        "source",
16        "sink",
17        "transform",
18        "dlq",
19        "replication",
20        "backfill",
21        "partition",
22        "execution",
23        "resilience",
24        "sla",
25    ];
26    #[cfg(feature = "quality")]
27    targets.push("quality");
28    #[cfg(feature = "contract")]
29    targets.push("contract");
30    #[cfg(feature = "masking")]
31    targets.push("masking");
32    targets.push("test");
33    targets.push("secrets");
34    #[cfg(feature = "schedule")]
35    targets.push("schedule");
36    #[cfg(feature = "lineage")]
37    targets.push("lineage");
38    #[cfg(feature = "triggers")]
39    targets.push("triggers");
40    #[cfg(feature = "notify")]
41    targets.push("notifications");
42    #[cfg(feature = "catalog")]
43    targets.push("catalog");
44    targets.push("params");
45    targets
46}
47
48/// Execute the `schema` subcommand.
49pub async fn run(args: SchemaArgs) -> CliResult<()> {
50    if args.list {
51        println!("Valid `faucet schema <target>` targets:");
52        for t in schema_targets() {
53            match t {
54                "source" | "sink" | "transform" => println!("  {t} <name>"),
55                _ => println!("  {t}"),
56            }
57        }
58        return Ok(());
59    }
60    let target = args.target.ok_or_else(|| {
61        CliError::Config(
62            "no schema target given — pass one (e.g. `faucet schema source rest`) or \
63             `faucet schema --list` to see them all"
64                .to_owned(),
65        )
66    })?;
67    let schema = match target {
68        SchemaTarget::Config => crate::schema_compose::config_schema(),
69        SchemaTarget::Source { name } => source_schema(&name)?,
70        SchemaTarget::Sink { name } => sink_schema(&name)?,
71        SchemaTarget::Transform { name } => transform_schema(&name)?,
72        SchemaTarget::Dlq => {
73            let dlq_schema = faucet_core::schema_for!(crate::config::DlqSpec);
74            serde_json::to_value(dlq_schema)
75                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
76        }
77        SchemaTarget::Replication => {
78            let s = faucet_core::schema_for!(crate::replication::spec::ReplicationSpec);
79            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
80        }
81        SchemaTarget::Backfill => {
82            let s = faucet_core::schema_for!(crate::backfill::BackfillSpec);
83            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
84        }
85        SchemaTarget::Partition => {
86            let s = faucet_core::schema_for!(crate::partition::PartitionSpec);
87            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
88        }
89        SchemaTarget::Params => {
90            let s = faucet_core::schema_for!(crate::params::ParamSpec);
91            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
92        }
93        SchemaTarget::Execution => {
94            let s = faucet_core::schema_for!(crate::config::ExecutionSpec);
95            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
96        }
97        SchemaTarget::Resilience => {
98            let s = faucet_core::schema_for!(crate::config::ResilienceSpec);
99            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
100        }
101        SchemaTarget::Sla => {
102            let s = faucet_core::schema_for!(crate::sla::SlaSpec);
103            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
104        }
105        #[cfg(feature = "quality")]
106        SchemaTarget::Quality => {
107            let quality_schema = faucet_core::schema_for!(faucet_core::QualitySpec);
108            serde_json::to_value(quality_schema)
109                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
110        }
111        #[cfg(feature = "contract")]
112        SchemaTarget::Contract => {
113            let contract_schema = faucet_core::schema_for!(faucet_core::ContractSpec);
114            serde_json::to_value(contract_schema)
115                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
116        }
117        #[cfg(feature = "masking")]
118        SchemaTarget::Masking => {
119            let masking_schema = faucet_core::schema_for!(faucet_core::MaskingSpec);
120            serde_json::to_value(masking_schema)
121                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
122        }
123        #[cfg(feature = "schedule")]
124        SchemaTarget::Schedule => {
125            let s = faucet_core::schema_for!(crate::schedule::spec::ScheduleSpec);
126            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
127        }
128        #[cfg(feature = "lineage")]
129        SchemaTarget::Lineage => lineage_schema(),
130        #[cfg(feature = "triggers")]
131        SchemaTarget::Triggers => {
132            let s = faucet_core::schema_for!(crate::serve::triggers::spec::TriggersFile);
133            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
134        }
135        SchemaTarget::Test => {
136            let s = faucet_core::schema_for!(crate::pipeline_test::spec::TestSpecFile);
137            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
138        }
139        #[cfg(feature = "notify")]
140        SchemaTarget::Notifications => {
141            // The `notifications:` block is a list; emit the per-rule schema.
142            let s = faucet_core::schema_for!(crate::notify::NotificationSpec);
143            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
144        }
145        #[cfg(feature = "catalog")]
146        SchemaTarget::Catalog => {
147            let s = faucet_core::schema_for!(crate::catalog::CatalogSpec);
148            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
149        }
150        SchemaTarget::Secrets => serde_json::json!({
151            "title": "Secrets-manager interpolation grammar",
152            "schemes": {
153                "vault":    { "syntax": "${vault:<path>[#field]}", "auth": ["VAULT_ADDR", "VAULT_TOKEN", "VAULT_NAMESPACE (optional)"] },
154                "aws-sm":   { "syntax": "${aws-sm:<name-or-ARN>[#field]}", "auth": ["aws-config default credential chain"] },
155                "gcp-sm":   { "syntax": "${gcp-sm:projects/<p>/secrets/<s>/versions/<v>}", "auth": ["Application Default Credentials"] },
156                "azure-kv": { "syntax": "${azure-kv:<vault>/<secret>[/<version>]}", "auth": ["AZURE_* env / managed identity / az login"] }
157            },
158            "notes": [
159                "#field parses the secret as JSON and extracts one key (vault, aws-sm).",
160                "Resolved at config load; fetched concurrently and de-duplicated; never persisted.",
161                "Build with --features secrets (or per-backend secrets-vault / secrets-aws-sm / ...)."
162            ]
163        }),
164    };
165    let body = serde_json::to_string_pretty(&schema).unwrap_or_else(|_| schema.to_string());
166    println!("{body}");
167    Ok(())
168}
169
170/// JSON Schema for the `lineage:` config block (`faucet schema lineage`).
171#[cfg(feature = "lineage")]
172pub fn lineage_schema() -> serde_json::Value {
173    serde_json::to_value(faucet_lineage::schemars_schema())
174        .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
175}
176
177#[cfg(test)]
178mod tests {
179    use crate::cli::{SchemaArgs, SchemaTarget};
180
181    #[cfg(feature = "lineage")]
182    #[test]
183    fn schema_lineage_returns_object_schema() {
184        let v = super::lineage_schema();
185        assert_eq!(v["type"], "object");
186        assert!(v["properties"].get("transport").is_some());
187        assert!(v["properties"].get("namespace").is_some());
188    }
189
190    #[test]
191    fn schema_targets_includes_known_targets() {
192        let targets = super::schema_targets();
193        for known in ["config", "source", "sink", "dlq", "params"] {
194            assert!(
195                targets.contains(&known),
196                "missing target {known}: {targets:?}"
197            );
198        }
199        // No duplicates.
200        let mut sorted = targets.clone();
201        sorted.sort_unstable();
202        sorted.dedup();
203        assert_eq!(
204            sorted.len(),
205            targets.len(),
206            "duplicate targets: {targets:?}"
207        );
208    }
209
210    #[tokio::test]
211    async fn schema_list_flag_returns_ok() {
212        let r = super::run(SchemaArgs {
213            target: None,
214            list: true,
215        })
216        .await;
217        assert!(r.is_ok(), "{r:?}");
218    }
219
220    #[tokio::test]
221    async fn schema_no_target_without_list_errors() {
222        let r = super::run(SchemaArgs {
223            target: None,
224            list: false,
225        })
226        .await;
227        assert!(
228            r.is_err(),
229            "expected an error when neither target nor --list given"
230        );
231    }
232
233    #[tokio::test]
234    async fn schema_replication_target_ok() {
235        // Covers the `SchemaTarget::Replication` arm — it serializes the
236        // ReplicationSpec JSON Schema to stdout and returns Ok.
237        let r = super::run(SchemaArgs {
238            target: Some(SchemaTarget::Replication),
239            list: false,
240        })
241        .await;
242        assert!(r.is_ok(), "{r:?}");
243    }
244
245    #[tokio::test]
246    async fn schema_execution_target_ok() {
247        let r = super::run(SchemaArgs {
248            target: Some(SchemaTarget::Execution),
249            list: false,
250        })
251        .await;
252        assert!(r.is_ok(), "{r:?}");
253    }
254
255    #[test]
256    fn execution_schema_includes_adaptive_batch_size() {
257        let schema = faucet_core::schema_for!(crate::config::ExecutionSpec);
258        let value = serde_json::to_value(schema).expect("execution schema serializes");
259        assert!(value["properties"].get("adaptive_batch_size").is_some());
260    }
261
262    #[tokio::test]
263    async fn schema_sla_target_ok() {
264        let r = super::run(SchemaArgs {
265            target: Some(SchemaTarget::Sla),
266            list: false,
267        })
268        .await;
269        assert!(r.is_ok(), "{r:?}");
270    }
271
272    #[test]
273    fn sla_schema_exposes_the_three_checks() {
274        let schema = faucet_core::schema_for!(crate::sla::SlaSpec);
275        let out = serde_json::to_string(&schema).expect("sla schema serializes");
276        assert!(out.contains("max_staleness_secs"), "{out}");
277        assert!(out.contains("min_rows_per_run"), "{out}");
278        assert!(out.contains("volume_anomaly"), "{out}");
279    }
280
281    #[tokio::test]
282    async fn schema_resilience_target_ok() {
283        let r = super::run(SchemaArgs {
284            target: Some(SchemaTarget::Resilience),
285            list: false,
286        })
287        .await;
288        assert!(r.is_ok(), "{r:?}");
289    }
290
291    #[test]
292    fn schema_resilience_emits_json_schema() {
293        // Mirrors `faucet schema resilience`: the serialized ResilienceSpec
294        // schema must expose the retry `max_attempts` knob and the
295        // `circuit_breaker` sub-block.
296        let schema = faucet_core::schema_for!(crate::config::ResilienceSpec);
297        let out = serde_json::to_string(&schema).expect("resilience schema serializes");
298        assert!(out.contains("max_attempts"), "{out}");
299        assert!(out.contains("circuit_breaker"), "{out}");
300    }
301}