use axum::http::{header, HeaderMap, HeaderValue};
use serde_json::Value;
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) static TRACE_COUNTER: AtomicU64 = AtomicU64::new(1);
pub fn next_trace_id() -> String {
format!("trace-{}", TRACE_COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn check_if_none_match(req_headers: &HeaderMap, catalog_version: impl AsRef<str>) -> bool {
let version_ref = catalog_version.as_ref();
if let Some(if_none_match) = req_headers.get(header::IF_NONE_MATCH) {
if let Ok(val) = if_none_match.to_str() {
let val_clean = val.trim();
if val_clean == "*" {
return true;
}
let unquoted = val_clean.trim_matches('"');
return unquoted == version_ref;
}
}
false
}
pub fn make_etag_header(catalog_version: impl AsRef<str>) -> HeaderMap {
let mut headers = HeaderMap::new();
let etag_val = format!("\"{}\"", catalog_version.as_ref());
if let Ok(hv) = HeaderValue::from_str(&etag_val) {
headers.insert(header::ETAG, hv);
}
headers
}
#[derive(serde::Deserialize, Debug, Clone, Default)]
pub struct ProfileQuery {
#[serde(default)]
pub profile: Option<String>,
}
pub async fn resolve_profile_context(
state: &crate::daemon::AppState,
headers: &HeaderMap,
query: Option<&ProfileQuery>,
) -> Result<crate::context::ProfileContext, (axum::http::StatusCode, Value)> {
let requested_profile = crate::context::extract_header_str(headers, "x-warmplane-profile")
.or_else(|| {
query
.and_then(|q| q.profile.as_ref().map(|p| p.trim().to_string()))
.filter(|s| !s.is_empty())
});
let Some(profile_id) = requested_profile else {
return Ok(crate::context::ProfileContext::unrestricted());
};
let profiles_guard = state.profiles.read().await;
match profiles_guard.get(&profile_id) {
Some(profile_cfg) => Ok(crate::context::ProfileContext::scoped(
profile_id,
profile_cfg.servers.clone(),
)),
None => {
let trace_id = next_trace_id();
let err_val = crate::http_v1::types::error_envelope(
trace_id,
None,
None,
crate::idempotency::RetryMetadata::safe("not_started"),
"PROFILE_NOT_FOUND",
format!("Profile '{}' is not defined in configuration", profile_id),
false,
);
Err((axum::http::StatusCode::NOT_FOUND, err_val))
}
}
}
pub fn get_profile_scoped_catalog_version(
base_version: impl AsRef<str>,
profile_ctx: &crate::context::ProfileContext,
) -> String {
match &profile_ctx.profile_id {
Some(pid) => format!("{}-p:{}", base_version.as_ref(), pid),
None => base_version.as_ref().to_string(),
}
}
pub fn resolve_idempotency_key(payload_key: Option<String>, headers: &HeaderMap) -> Option<String> {
if let Some(k) = payload_key.filter(|s| !s.trim().is_empty()) {
return Some(k);
}
crate::context::extract_header_str(headers, "idempotency-key")
.or_else(|| crate::context::extract_header_str(headers, "x-idempotency-key"))
}
pub const DEFAULT_REDACT_KEYS: &[&str] = &[
"token",
"api_key",
"apikey",
"password",
"secret",
"authorization",
"auth",
"access_token",
"private_key",
"bearer",
];
pub fn redact_value(value: Value, redact_keys: &[String]) -> Value {
let lower_keys: Vec<String> = redact_keys.iter().map(|k| k.to_lowercase()).collect();
redact_value_internal(value, &lower_keys)
}
fn redact_value_internal(value: Value, lower_keys: &[String]) -> Value {
match value {
Value::Object(map) => {
let mut output = serde_json::Map::new();
for (key, nested) in map {
let key_lower = key.to_lowercase();
let should_redact = lower_keys.iter().any(|k| k == &key_lower)
|| DEFAULT_REDACT_KEYS.iter().any(|&k| k == key_lower);
if should_redact {
output.insert(key, Value::String("<redacted>".to_string()));
} else {
output.insert(key, redact_value_internal(nested, lower_keys));
}
}
Value::Object(output)
}
Value::Array(values) => Value::Array(
values
.into_iter()
.map(|entry| redact_value_internal(entry, lower_keys))
.collect(),
),
primitive => primitive,
}
}