use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const RESOLVE_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResolvedSource {
Provider,
Generated,
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedSecret {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub as_path: bool,
pub source: ResolvedSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_provider: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolveResponse {
pub schema_version: u32,
pub provider: String,
pub profile: String,
pub secrets: BTreeMap<String, ResolvedSecret>,
pub missing_required: Vec<String>,
pub missing_optional: Vec<String>,
}
impl ResolveResponse {
pub fn is_ok(&self) -> bool {
self.missing_required.is_empty()
}
pub fn without_values(mut self) -> Self {
for secret in self.secrets.values_mut() {
secret.value = None;
secret.path = None;
}
self
}
}
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum RequestMode {
#[default]
Resolve,
Report,
}
#[derive(Debug, Default, Deserialize)]
struct JsonRequest {
#[serde(default)]
path: Option<String>,
#[serde(default)]
provider: Option<String>,
#[serde(default)]
profile: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
no_values: bool,
#[serde(default)]
mode: RequestMode,
}
fn error_envelope(kind: &str, message: impl Into<String>) -> serde_json::Value {
serde_json::json!({
"ok": false,
"error": { "kind": kind, "message": message.into() },
})
}
fn ok_envelope(response: impl Serialize) -> serde_json::Value {
serde_json::json!({ "ok": true, "response": response })
}
fn dispatch(request_json: &str) -> serde_json::Value {
let request: JsonRequest = match serde_json::from_str(request_json) {
Ok(request) => request,
Err(e) => return error_envelope("invalid_request", format!("invalid request JSON: {e}")),
};
let loaded = match &request.path {
Some(path) => crate::Secrets::load_from(std::path::Path::new(path)),
None => crate::Secrets::load(),
};
let mut app = match loaded {
Ok(app) => app,
Err(e) => return error_envelope(e.kind(), e.to_string()),
};
if let Some(provider) = request.provider {
app.set_provider(provider);
}
if let Some(profile) = request.profile {
app.set_profile(profile);
}
if let Some(reason) = request.reason {
app = app.with_reason(reason);
}
match request.mode {
RequestMode::Report => match app.report() {
Ok(report) => ok_envelope(report),
Err(e) => error_envelope(e.kind(), e.to_string()),
},
RequestMode::Resolve => {
let resolved = if request.no_values {
app.resolve_without_values()
} else {
app.resolve()
};
match resolved {
Ok(response) => ok_envelope(response),
Err(e) => error_envelope(e.kind(), e.to_string()),
}
}
}
}
pub fn resolve_json(request_json: &str) -> String {
let envelope =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| dispatch(request_json)))
.unwrap_or_else(|_| error_envelope("internal", "internal panic during resolve"));
serde_json::to_string(&envelope).unwrap_or_else(|_| {
"{\"ok\":false,\"error\":{\"kind\":\"serialize\",\"message\":\"failed to serialize response\"}}".to_string()
})
}