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