#![cfg_attr(docsrs, feature(doc_cfg))]
use faucet_core::FaucetError;
use gcp_bigquery_client::Client;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum BigQueryCredentials {
ServiceAccountKeyPath {
path: String,
},
ServiceAccountKey {
json: String,
},
ApplicationDefault,
}
impl std::fmt::Debug for BigQueryCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ServiceAccountKeyPath { path } => f
.debug_struct("ServiceAccountKeyPath")
.field("path", path)
.finish(),
Self::ServiceAccountKey { .. } => write!(f, "ServiceAccountKey(***)"),
Self::ApplicationDefault => write!(f, "ApplicationDefault"),
}
}
}
pub async fn build_client(creds: &BigQueryCredentials) -> Result<Client, FaucetError> {
match creds {
BigQueryCredentials::ServiceAccountKeyPath { path } => {
Client::from_service_account_key_file(path)
.await
.map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}")))
}
BigQueryCredentials::ServiceAccountKey { json } => {
let sa_key = serde_json::from_str(json)
.map_err(|e| FaucetError::Auth(format!("invalid service account JSON: {e}")))?;
Client::from_service_account_key(sa_key, false)
.await
.map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}")))
}
BigQueryCredentials::ApplicationDefault => Client::from_application_default_credentials()
.await
.map_err(|e| FaucetError::Auth(format!("BigQuery auth failed: {e}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_masks_inline_service_account_key() {
let creds = BigQueryCredentials::ServiceAccountKey {
json: "secret-json".into(),
};
let debug = format!("{creds:?}");
assert!(debug.contains("***"));
assert!(!debug.contains("secret-json"));
}
#[test]
fn debug_does_not_mask_service_account_key_path() {
let creds = BigQueryCredentials::ServiceAccountKeyPath {
path: "/path/to/key.json".into(),
};
let debug = format!("{creds:?}");
assert!(debug.contains("/path/to/key.json"));
}
#[test]
fn debug_application_default_is_plain() {
let creds = BigQueryCredentials::ApplicationDefault;
assert_eq!(format!("{creds:?}"), "ApplicationDefault");
}
#[test]
fn serde_round_trip_application_default() {
let json = serde_json::to_string(&BigQueryCredentials::ApplicationDefault).unwrap();
let parsed: BigQueryCredentials = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, BigQueryCredentials::ApplicationDefault));
}
#[test]
fn serde_round_trip_service_account_key_path() {
let creds = BigQueryCredentials::ServiceAccountKeyPath {
path: "/k.json".into(),
};
let json = serde_json::to_string(&creds).unwrap();
assert_eq!(
json,
r#"{"type":"service_account_key_path","config":{"path":"/k.json"}}"#
);
let parsed: BigQueryCredentials = serde_json::from_str(&json).unwrap();
match parsed {
BigQueryCredentials::ServiceAccountKeyPath { path } => assert_eq!(path, "/k.json"),
_ => panic!("expected ServiceAccountKeyPath"),
}
}
#[tokio::test]
async fn build_client_with_invalid_inline_json_surfaces_auth_error() {
let creds = BigQueryCredentials::ServiceAccountKey {
json: "not-json".into(),
};
match build_client(&creds).await {
Ok(_) => panic!("expected auth error"),
Err(FaucetError::Auth(_)) => {}
Err(other) => panic!("expected Auth error, got {other:?}"),
}
}
}