faucet_cli/commands/
schema.rs1use crate::cli::{SchemaArgs, SchemaTarget};
4use crate::error::{CliError, CliResult};
5use crate::registry::{sink_schema, source_schema};
6use crate::transforms::transform_schema;
7
8pub 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
48pub 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 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#[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 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 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 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}