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::CliResult;
5use crate::registry::{sink_schema, source_schema};
6use crate::transforms::transform_schema;
7
8/// Execute the `schema` subcommand.
9pub async fn run(args: SchemaArgs) -> CliResult<()> {
10    let schema = match args.target {
11        SchemaTarget::Config => crate::schema_compose::config_schema(),
12        SchemaTarget::Source { name } => source_schema(&name)?,
13        SchemaTarget::Sink { name } => sink_schema(&name)?,
14        SchemaTarget::Transform { name } => transform_schema(&name)?,
15        SchemaTarget::Dlq => {
16            let dlq_schema = faucet_core::schema_for!(crate::config::DlqSpec);
17            serde_json::to_value(dlq_schema)
18                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
19        }
20        SchemaTarget::Replication => {
21            let s = faucet_core::schema_for!(crate::replication::spec::ReplicationSpec);
22            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
23        }
24        SchemaTarget::Backfill => {
25            let s = faucet_core::schema_for!(crate::backfill::BackfillSpec);
26            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
27        }
28        SchemaTarget::Partition => {
29            let s = faucet_core::schema_for!(crate::partition::PartitionSpec);
30            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
31        }
32        SchemaTarget::Params => {
33            let s = faucet_core::schema_for!(crate::params::ParamSpec);
34            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
35        }
36        SchemaTarget::Execution => {
37            let s = faucet_core::schema_for!(crate::config::ExecutionSpec);
38            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
39        }
40        SchemaTarget::Resilience => {
41            let s = faucet_core::schema_for!(crate::config::ResilienceSpec);
42            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
43        }
44        SchemaTarget::Sla => {
45            let s = faucet_core::schema_for!(crate::sla::SlaSpec);
46            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
47        }
48        #[cfg(feature = "quality")]
49        SchemaTarget::Quality => {
50            let quality_schema = faucet_core::schema_for!(faucet_core::QualitySpec);
51            serde_json::to_value(quality_schema)
52                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
53        }
54        #[cfg(feature = "contract")]
55        SchemaTarget::Contract => {
56            let contract_schema = faucet_core::schema_for!(faucet_core::ContractSpec);
57            serde_json::to_value(contract_schema)
58                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
59        }
60        #[cfg(feature = "masking")]
61        SchemaTarget::Masking => {
62            let masking_schema = faucet_core::schema_for!(faucet_core::MaskingSpec);
63            serde_json::to_value(masking_schema)
64                .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
65        }
66        #[cfg(feature = "schedule")]
67        SchemaTarget::Schedule => {
68            let s = faucet_core::schema_for!(crate::schedule::spec::ScheduleSpec);
69            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
70        }
71        #[cfg(feature = "lineage")]
72        SchemaTarget::Lineage => lineage_schema(),
73        #[cfg(feature = "triggers")]
74        SchemaTarget::Triggers => {
75            let s = faucet_core::schema_for!(crate::serve::triggers::spec::TriggersFile);
76            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
77        }
78        SchemaTarget::Test => {
79            let s = faucet_core::schema_for!(crate::pipeline_test::spec::TestSpecFile);
80            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
81        }
82        #[cfg(feature = "notify")]
83        SchemaTarget::Notifications => {
84            // The `notifications:` block is a list; emit the per-rule schema.
85            let s = faucet_core::schema_for!(crate::notify::NotificationSpec);
86            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
87        }
88        #[cfg(feature = "catalog")]
89        SchemaTarget::Catalog => {
90            let s = faucet_core::schema_for!(crate::catalog::CatalogSpec);
91            serde_json::to_value(s).unwrap_or_else(|_| serde_json::json!({"type": "object"}))
92        }
93        SchemaTarget::Secrets => serde_json::json!({
94            "title": "Secrets-manager interpolation grammar",
95            "schemes": {
96                "vault":    { "syntax": "${vault:<path>[#field]}", "auth": ["VAULT_ADDR", "VAULT_TOKEN", "VAULT_NAMESPACE (optional)"] },
97                "aws-sm":   { "syntax": "${aws-sm:<name-or-ARN>[#field]}", "auth": ["aws-config default credential chain"] },
98                "gcp-sm":   { "syntax": "${gcp-sm:projects/<p>/secrets/<s>/versions/<v>}", "auth": ["Application Default Credentials"] },
99                "azure-kv": { "syntax": "${azure-kv:<vault>/<secret>[/<version>]}", "auth": ["AZURE_* env / managed identity / az login"] }
100            },
101            "notes": [
102                "#field parses the secret as JSON and extracts one key (vault, aws-sm).",
103                "Resolved at config load; fetched concurrently and de-duplicated; never persisted.",
104                "Build with --features secrets (or per-backend secrets-vault / secrets-aws-sm / ...)."
105            ]
106        }),
107    };
108    let body = serde_json::to_string_pretty(&schema).unwrap_or_else(|_| schema.to_string());
109    println!("{body}");
110    Ok(())
111}
112
113/// JSON Schema for the `lineage:` config block (`faucet schema lineage`).
114#[cfg(feature = "lineage")]
115pub fn lineage_schema() -> serde_json::Value {
116    serde_json::to_value(faucet_lineage::schemars_schema())
117        .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::cli::{SchemaArgs, SchemaTarget};
123
124    #[cfg(feature = "lineage")]
125    #[test]
126    fn schema_lineage_returns_object_schema() {
127        let v = super::lineage_schema();
128        assert_eq!(v["type"], "object");
129        assert!(v["properties"].get("transport").is_some());
130        assert!(v["properties"].get("namespace").is_some());
131    }
132
133    #[tokio::test]
134    async fn schema_replication_target_ok() {
135        // Covers the `SchemaTarget::Replication` arm — it serializes the
136        // ReplicationSpec JSON Schema to stdout and returns Ok.
137        let r = super::run(SchemaArgs {
138            target: SchemaTarget::Replication,
139        })
140        .await;
141        assert!(r.is_ok(), "{r:?}");
142    }
143
144    #[tokio::test]
145    async fn schema_execution_target_ok() {
146        let r = super::run(SchemaArgs {
147            target: SchemaTarget::Execution,
148        })
149        .await;
150        assert!(r.is_ok(), "{r:?}");
151    }
152
153    #[test]
154    fn execution_schema_includes_adaptive_batch_size() {
155        let schema = faucet_core::schema_for!(crate::config::ExecutionSpec);
156        let value = serde_json::to_value(schema).expect("execution schema serializes");
157        assert!(value["properties"].get("adaptive_batch_size").is_some());
158    }
159
160    #[tokio::test]
161    async fn schema_sla_target_ok() {
162        let r = super::run(SchemaArgs {
163            target: SchemaTarget::Sla,
164        })
165        .await;
166        assert!(r.is_ok(), "{r:?}");
167    }
168
169    #[test]
170    fn sla_schema_exposes_the_three_checks() {
171        let schema = faucet_core::schema_for!(crate::sla::SlaSpec);
172        let out = serde_json::to_string(&schema).expect("sla schema serializes");
173        assert!(out.contains("max_staleness_secs"), "{out}");
174        assert!(out.contains("min_rows_per_run"), "{out}");
175        assert!(out.contains("volume_anomaly"), "{out}");
176    }
177
178    #[tokio::test]
179    async fn schema_resilience_target_ok() {
180        let r = super::run(SchemaArgs {
181            target: SchemaTarget::Resilience,
182        })
183        .await;
184        assert!(r.is_ok(), "{r:?}");
185    }
186
187    #[test]
188    fn schema_resilience_emits_json_schema() {
189        // Mirrors `faucet schema resilience`: the serialized ResilienceSpec
190        // schema must expose the retry `max_attempts` knob and the
191        // `circuit_breaker` sub-block.
192        let schema = faucet_core::schema_for!(crate::config::ResilienceSpec);
193        let out = serde_json::to_string(&schema).expect("resilience schema serializes");
194        assert!(out.contains("max_attempts"), "{out}");
195        assert!(out.contains("circuit_breaker"), "{out}");
196    }
197}