Skip to main content

faucet_common_bigquery/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! # faucet-common-bigquery
4//!
5//! Shared credential configuration and client construction for the
6//! [`faucet-stream`](https://crates.io/crates/faucet-stream) BigQuery source
7//! and sink connectors.
8//!
9//! - [`BigQueryCredentials`] — service-account key file, inline service-account
10//!   JSON, or Application Default Credentials.
11//! - [`build_client`] — async helper that turns a [`BigQueryCredentials`] into
12//!   a ready-to-use [`gcp_bigquery_client::Client`].
13//!
14//! `BigQueryCredentials` derives `Serialize`, `Deserialize`, and `JsonSchema`
15//! so it round-trips through YAML/JSON configs and CLI introspection. Its
16//! `Debug` impl masks inline JSON as `"***"` while leaving the key path
17//! visible.
18
19use faucet_core::FaucetError;
20use gcp_bigquery_client::Client;
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23
24/// How to authenticate with Google BigQuery.
25///
26/// Serializes as `{ type: <method>, config: { … } }` (adjacent tagging,
27/// snake_case discriminators) — the consistent auth wire shape shared by
28/// every faucet connector.
29#[derive(Clone, Serialize, Deserialize, JsonSchema)]
30#[serde(tag = "type", content = "config", rename_all = "snake_case")]
31pub enum BigQueryCredentials {
32    /// Path to a service account JSON key file.
33    ServiceAccountKeyPath {
34        /// Filesystem path to the service-account JSON key.
35        path: String,
36    },
37    /// Inline service account JSON key content.
38    ServiceAccountKey {
39        /// Service-account JSON key as an inline string.
40        json: String,
41    },
42    /// Use application default credentials (e.g. workload identity, `gcloud auth`).
43    ApplicationDefault,
44}
45
46impl std::fmt::Debug for BigQueryCredentials {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::ServiceAccountKeyPath { path } => f
50                .debug_struct("ServiceAccountKeyPath")
51                .field("path", path)
52                .finish(),
53            Self::ServiceAccountKey { .. } => write!(f, "ServiceAccountKey(***)"),
54            Self::ApplicationDefault => write!(f, "ApplicationDefault"),
55        }
56    }
57}
58
59/// Build a [`gcp_bigquery_client::Client`] from a faucet credential spec.
60///
61/// Returns [`FaucetError::Auth`] on authentication failures and on inline
62/// service-account JSON that fails to parse.
63pub async fn build_client(creds: &BigQueryCredentials) -> Result<Client, FaucetError> {
64    match creds {
65        BigQueryCredentials::ServiceAccountKeyPath { path } => {
66            Client::from_service_account_key_file(path)
67                .await
68                .map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}")))
69        }
70        BigQueryCredentials::ServiceAccountKey { json } => {
71            let sa_key = serde_json::from_str(json)
72                .map_err(|e| FaucetError::Auth(format!("invalid service account JSON: {e}")))?;
73            Client::from_service_account_key(sa_key, false)
74                .await
75                .map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}")))
76        }
77        BigQueryCredentials::ApplicationDefault => Client::from_application_default_credentials()
78            .await
79            .map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}"))),
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn debug_masks_inline_service_account_key() {
89        let creds = BigQueryCredentials::ServiceAccountKey {
90            json: "secret-json".into(),
91        };
92        let debug = format!("{creds:?}");
93        assert!(debug.contains("***"));
94        assert!(!debug.contains("secret-json"));
95    }
96
97    #[test]
98    fn debug_does_not_mask_service_account_key_path() {
99        let creds = BigQueryCredentials::ServiceAccountKeyPath {
100            path: "/path/to/key.json".into(),
101        };
102        let debug = format!("{creds:?}");
103        assert!(debug.contains("/path/to/key.json"));
104    }
105
106    #[test]
107    fn debug_application_default_is_plain() {
108        let creds = BigQueryCredentials::ApplicationDefault;
109        assert_eq!(format!("{creds:?}"), "ApplicationDefault");
110    }
111
112    #[test]
113    fn serde_round_trip_application_default() {
114        let json = serde_json::to_string(&BigQueryCredentials::ApplicationDefault).unwrap();
115        let parsed: BigQueryCredentials = serde_json::from_str(&json).unwrap();
116        assert!(matches!(parsed, BigQueryCredentials::ApplicationDefault));
117    }
118
119    #[test]
120    fn serde_round_trip_service_account_key_path() {
121        let creds = BigQueryCredentials::ServiceAccountKeyPath {
122            path: "/k.json".into(),
123        };
124        let json = serde_json::to_string(&creds).unwrap();
125        assert_eq!(
126            json,
127            r#"{"type":"service_account_key_path","config":{"path":"/k.json"}}"#
128        );
129        let parsed: BigQueryCredentials = serde_json::from_str(&json).unwrap();
130        match parsed {
131            BigQueryCredentials::ServiceAccountKeyPath { path } => assert_eq!(path, "/k.json"),
132            _ => panic!("expected ServiceAccountKeyPath"),
133        }
134    }
135
136    #[tokio::test]
137    async fn build_client_with_invalid_inline_json_surfaces_auth_error() {
138        let creds = BigQueryCredentials::ServiceAccountKey {
139            json: "not-json".into(),
140        };
141        match build_client(&creds).await {
142            Ok(_) => panic!("expected auth error"),
143            Err(FaucetError::Auth(_)) => {}
144            Err(other) => panic!("expected Auth error, got {other:?}"),
145        }
146    }
147}