use std::collections::{HashMap, HashSet};
use faucet_cli::auth_catalog::AuthCatalog;
use faucet_cli::registry;
use serde_json::{Map, Value, json};
use std::time::Duration;
const BUILD_TIMEOUT: Duration = Duration::from_secs(4);
const MUST_CHECK_SINKS: &[&str] = &["jsonl", "csv", "stdout", "sqlite"];
const MUST_CHECK_SOURCES: &[&str] = &["csv", "sqlite"];
fn config_override(key: &str) -> Option<Value> {
let v = match key {
"sink:sqlite" => json!({ "database_url": "sqlite::memory:" }),
"source:sqlite" => json!({ "database_url": "sqlite::memory:", "query": "SELECT 1" }),
"sink:mongodb" | "source:mongodb" => {
json!({ "connection_uri": "mongodb://localhost:27017" })
}
"sink:redis" | "source:redis" => json!({ "url": "redis://localhost:6379" }),
_ => return None,
};
Some(v)
}
fn deep_merge(base: &mut Value, overlay: Value) {
match (base, overlay) {
(Value::Object(b), Value::Object(o)) => {
for (k, v) in o {
deep_merge(b.entry(k).or_insert(Value::Null), v);
}
}
(b, o) => *b = o,
}
}
fn synthesize(schema: &Value, defs: &Map<String, Value>, field: &str, depth: u32) -> Value {
if depth > 12 {
return Value::Null;
}
if let Some(r) = schema.get("$ref").and_then(Value::as_str) {
let name = r.rsplit('/').next().unwrap_or(r);
return match defs.get(name) {
Some(target) => synthesize(target, defs, field, depth + 1),
None => Value::Null,
};
}
if let Some(c) = schema.get("const") {
return c.clone();
}
if let Some(first) = schema
.get("enum")
.and_then(Value::as_array)
.and_then(|a| a.first())
{
return first.clone();
}
for combinator in ["oneOf", "anyOf", "allOf"] {
if let Some(first) = schema
.get(combinator)
.and_then(Value::as_array)
.and_then(|a| a.first())
{
return synthesize(first, defs, field, depth + 1);
}
}
let ty = schema.get("type").and_then(|t| match t {
Value::String(s) => Some(s.clone()),
Value::Array(a) => a
.iter()
.filter_map(Value::as_str)
.find(|s| *s != "null")
.map(String::from),
_ => None,
});
let is_object =
ty.as_deref() == Some("object") || (ty.is_none() && schema.get("properties").is_some());
if is_object {
let props = schema
.get("properties")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let required: Vec<String> = schema
.get("required")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
.unwrap_or_default();
let mut obj = Map::new();
for name in &required {
if let Some(prop) = props.get(name) {
obj.insert(name.clone(), synthesize(prop, defs, name, depth + 1));
}
}
return Value::Object(obj);
}
match ty.as_deref() {
Some("string") => Value::String(placeholder_string(field)),
Some("integer") | Some("number") => json!(1),
Some("boolean") => json!(false),
Some("array") => json!([]),
_ => Value::Null,
}
}
fn placeholder_string(field: &str) -> String {
let f = field.to_ascii_lowercase();
if f.contains("broker") || f.contains("bootstrap") {
"localhost:9092".into()
} else if f.contains("url") || f.contains("uri") || f.contains("endpoint") {
"http://localhost:1/".into()
} else if f.contains("host") {
"localhost".into()
} else if f.contains("path") || f.contains("file") {
"/tmp/faucet_parity_probe".into()
} else if f.contains("region") {
"us-east-1".into()
} else {
"x".into()
}
}
fn minimal_config(kind_key: &str, schema: &Value) -> Value {
let empty = Map::new();
let defs = schema
.get("$defs")
.or_else(|| schema.get("definitions"))
.and_then(Value::as_object)
.unwrap_or(&empty);
let mut cfg = synthesize(schema, defs, "", 0);
if let Some(overlay) = config_override(kind_key) {
deep_merge(&mut cfg, overlay);
}
cfg
}
async fn probe_sink(kind: &'static str) -> bool {
let Ok(schema) = registry::sink_schema(kind) else {
return false;
};
let cfg = minimal_config(&format!("sink:{kind}"), &schema);
let build = tokio::spawn(async move {
let auth: AuthCatalog = HashMap::new();
registry::build_sink(kind, cfg, &auth).await
});
let sink = match tokio::time::timeout(BUILD_TIMEOUT, build).await {
Ok(Ok(Ok(s))) => s,
_ => return false,
};
assert_eq!(
registry::sink_supports_idempotent_writes(kind),
sink.supports_idempotent_writes(),
"{kind}: IDEMPOTENT_SINK_KINDS ({}) disagrees with Sink::supports_idempotent_writes ({})",
registry::sink_supports_idempotent_writes(kind),
sink.supports_idempotent_writes(),
);
assert_eq!(
registry::sink_supports_schema_evolution(kind),
sink.supports_schema_evolution(),
"{kind}: SCHEMA_EVOLUTION_SINK_KINDS disagrees with Sink::supports_schema_evolution",
);
assert_eq!(
registry::sink_supported_write_modes(kind),
sink.supported_write_modes(),
"{kind}: sink_supported_write_modes() disagrees with Sink::supported_write_modes()",
);
true
}
async fn probe_source(kind: &'static str) -> bool {
let Ok(schema) = registry::source_schema(kind) else {
return false;
};
let cfg = minimal_config(&format!("source:{kind}"), &schema);
let build = tokio::spawn(async move {
let auth: AuthCatalog = HashMap::new();
registry::build_source(kind, cfg, &auth, None).await
});
let source = match tokio::time::timeout(BUILD_TIMEOUT, build).await {
Ok(Ok(Ok(s))) => s,
_ => return false,
};
assert_eq!(
registry::source_supports_exactly_once(kind),
source.supports_exactly_once(),
"{kind}: EXACTLY_ONCE_SOURCE_KINDS disagrees with Source::supports_exactly_once",
);
assert_eq!(
registry::source_supports_discover(kind),
source.supports_discover(),
"{kind}: DISCOVER_SOURCE_KINDS disagrees with Source::supports_discover",
);
true
}
#[tokio::test(flavor = "multi_thread")]
async fn sink_allowlists_match_trait_methods() {
let mut checked: HashSet<&str> = HashSet::new();
for kind in registry::sink_kinds() {
if probe_sink(kind).await {
checked.insert(kind);
}
}
assert!(
!checked.is_empty(),
"no sinks were probed — the synthesizer is broken"
);
let compiled: HashSet<&str> = registry::sink_kinds().into_iter().collect();
for must in MUST_CHECK_SINKS {
if compiled.contains(must) {
assert!(
checked.contains(must),
"sink '{must}' is compiled and must be probeable offline, but was not \
checked — its config synthesizer/override regressed (offline build broke)"
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn source_allowlists_match_trait_methods() {
let mut checked: HashSet<&str> = HashSet::new();
for kind in registry::source_kinds() {
if probe_source(kind).await {
checked.insert(kind);
}
}
assert!(
!checked.is_empty(),
"no sources were probed — the synthesizer is broken"
);
let compiled: HashSet<&str> = registry::source_kinds().into_iter().collect();
for must in MUST_CHECK_SOURCES {
if compiled.contains(must) {
assert!(
checked.contains(must),
"source '{must}' is compiled and must be probeable offline, but was not \
checked — its config synthesizer/override regressed (offline build broke)"
);
}
}
}
#[test]
fn derived_capability_helpers_agree_with_allowlists() {
for kind in registry::IDEMPOTENT_SINK_KINDS {
assert!(registry::sink_supports_idempotent_writes(kind), "{kind}");
assert_eq!(
registry::sink_guarantee(kind),
faucet_core::SinkGuarantee::AtomicWatermark,
"{kind}: idempotent sink must derive AtomicWatermark"
);
}
for kind in registry::UPSERT_SINK_KINDS {
assert!(
registry::sink_supported_write_modes(kind).contains(&faucet_core::WriteMode::Upsert),
"{kind}: UPSERT_SINK_KINDS member must advertise Upsert"
);
}
for kind in registry::EXACTLY_ONCE_SOURCE_KINDS {
assert!(registry::source_supports_exactly_once(kind), "{kind}");
}
for kind in registry::DISCOVER_SOURCE_KINDS {
assert!(registry::source_supports_discover(kind), "{kind}");
}
}