use std::borrow::Cow;
use std::fmt;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Serialize};
use crate::adapter::SECRET_MARKER;
#[derive(Clone, PartialEq, Eq)]
pub enum Secret {
Value(String),
Env(String),
}
impl Secret {
pub fn env(var: impl Into<String>) -> Self {
Secret::Env(var.into())
}
fn read(&self) -> Result<String, ResolveError> {
match self {
Secret::Value(value) => Ok(value.clone()),
Secret::Env(var) => {
std::env::var(var).map_err(|_| ResolveError::EnvNotSet(var.clone()))
}
}
}
pub fn resolve(&self, override_var: &str) -> Result<String, ResolveError> {
resolve_required(self, override_var)
}
pub fn resolve_optional(
secret: Option<&Secret>,
override_var: &str,
) -> Result<Option<String>, ResolveError> {
resolve_optional(secret, override_var)
}
}
impl JsonSchema for Secret {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Secret")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
let mut schema = json_schema!({
"description": "A literal value, or `{ env = \"VAR\" }` to read it from the environment at run time.",
"anyOf": [
{ "type": "string" },
{
"type": "object",
"properties": { "env": { "type": "string", "description": "The environment variable to read." } },
"required": ["env"],
"additionalProperties": false
}
]
});
schema.insert(SECRET_MARKER.to_owned(), serde_json::Value::Bool(true));
schema
}
}
impl From<&str> for Secret {
fn from(value: &str) -> Self {
Secret::Value(value.to_owned())
}
}
impl From<String> for Secret {
fn from(value: String) -> Self {
Secret::Value(value)
}
}
const EXPECTED: &str = "a string value or an env reference `{ env = \"VAR\" }`";
impl Serialize for Secret {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Secret::Value(value) => serializer.serialize_str(value),
Secret::Env(env) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("env", env)?;
map.end()
}
}
}
}
struct SecretVisitor;
impl<'de> serde::de::Visitor<'de> for SecretVisitor {
type Value = Secret;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(EXPECTED)
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(Secret::Value(value.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
Ok(Secret::Value(value))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
use serde::de::Error;
let Some(key) = map.next_key::<String>()? else {
return Err(A::Error::custom(format!(
"expected {EXPECTED}, found an empty table"
)));
};
if key != "env" {
return Err(A::Error::custom(format!(
"unknown key `{key}` — expected {EXPECTED}"
)));
}
let env: String = map.next_value()?;
if let Some(extra) = map.next_key::<String>()? {
return Err(A::Error::custom(format!(
"unexpected key `{extra}` in env reference — write it as `{{ env = \"VAR\" }}`"
)));
}
Ok(Secret::Env(env))
}
}
impl<'de> Deserialize<'de> for Secret {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(SecretVisitor)
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Secret::Value(_) => write!(f, "Secret(***)"),
Secret::Env(var) => write!(f, "Secret(env {var})"),
}
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Secret::Value(_) => write!(f, "***"),
Secret::Env(var) => write!(f, "${{{var}}}"),
}
}
}
fn resolve_required(secret: &Secret, reserved: &str) -> Result<String, ResolveError> {
match secret {
env @ Secret::Env(_) => env.read(),
Secret::Value(literal) => Ok(literal_or_override(literal, reserved)),
}
}
fn resolve_optional(
secret: Option<&Secret>,
reserved: &str,
) -> Result<Option<String>, ResolveError> {
match secret {
Some(env @ Secret::Env(_)) => env.read().map(Some),
Some(Secret::Value(literal)) => Ok(Some(literal_or_override(literal, reserved))),
None => Ok(std::env::var(reserved).ok()),
}
}
fn literal_or_override(literal: &str, reserved: &str) -> String {
std::env::var(reserved).unwrap_or_else(|_| literal.to_owned())
}
#[derive(thiserror::Error, Debug)]
pub enum ResolveError {
#[error("environment variable '{0}' is not set")]
EnvNotSet(String),
#[error("{0}")]
Missing(String),
#[error("{0}")]
Invalid(String),
}