use std::marker::PhantomData;
use crate::{
error::Error,
platform::{MaybeSendBoxFuture, MaybeSendSync},
secrets::{Secret, SecretMap, SecretOutput},
};
#[derive(Clone)]
pub struct MappedSecret<S, M> {
inner: S,
map: M,
context: Option<String>,
}
impl<S, M> MappedSecret<S, M> {
pub fn new(inner: S, map: M) -> Self {
Self {
inner,
map,
context: None,
}
}
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
}
impl<S, M> std::fmt::Debug for MappedSecret<S, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MappedSecret").finish_non_exhaustive()
}
}
impl<S, M> Secret for MappedSecret<S, M>
where
S: Secret,
M: SecretMap<In = S::Output>,
{
type Output = M::Out;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
let input = self.inner.get_secret_value().await?;
let value = self.map.apply(input.value).map_err(|err| {
err.with_context(
self.context
.clone()
.unwrap_or_else(|| "mapping secret value".to_owned()),
)
})?;
Ok(SecretOutput {
value,
identity: input.identity,
})
})
}
}
pub struct FnMap<F, In> {
f: F,
_in: PhantomData<fn(In)>,
}
impl<F, In> FnMap<F, In> {
pub fn new(f: F) -> Self {
Self {
f,
_in: PhantomData,
}
}
}
impl<F: Clone, In> Clone for FnMap<F, In> {
fn clone(&self) -> Self {
Self::new(self.f.clone())
}
}
impl<F, In> std::fmt::Debug for FnMap<F, In> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnMap").finish_non_exhaustive()
}
}
impl<F, In, Out> SecretMap for FnMap<F, In>
where
F: Fn(In) -> Out + MaybeSendSync,
Out: Clone + MaybeSendSync,
{
type In = In;
type Out = Out;
fn apply(&self, input: In) -> Result<Out, Error> {
Ok((self.f)(input))
}
}
pub struct TryFnMap<F, In> {
f: F,
_in: PhantomData<fn(In)>,
}
impl<F, In> TryFnMap<F, In> {
pub fn new(f: F) -> Self {
Self {
f,
_in: PhantomData,
}
}
}
impl<F: Clone, In> Clone for TryFnMap<F, In> {
fn clone(&self) -> Self {
Self::new(self.f.clone())
}
}
impl<F, In> std::fmt::Debug for TryFnMap<F, In> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TryFnMap").finish_non_exhaustive()
}
}
impl<F, In, Out> SecretMap for TryFnMap<F, In>
where
F: Fn(In) -> Result<Out, Error> + MaybeSendSync,
Out: Clone + MaybeSendSync,
{
type In = In;
type Out = Out;
fn apply(&self, input: In) -> Result<Out, Error> {
(self.f)(input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
error::ErrorKind,
secrets::{
SecretBytes, SecretString, WithIdentity,
encodings::{Base64Encoding, StringToBytes},
},
};
#[derive(Clone)]
struct BytesSource(&'static [u8]);
impl Secret for BytesSource {
type Output = SecretBytes;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
Ok(SecretOutput {
value: SecretBytes::new(self.0.to_vec()),
identity: None,
})
})
}
}
#[derive(Clone)]
struct StringSource(&'static str);
impl Secret for StringSource {
type Output = SecretString;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
Ok(SecretOutput {
value: SecretString::new(self.0),
identity: None,
})
})
}
}
#[tokio::test]
async fn maps_inner_bytes_through_named_map() {
let secret = MappedSecret::new(BytesSource(b"aGVsbG8="), Base64Encoding);
let out = secret.get_secret_value().await.unwrap();
assert_eq!(out.value.expose_secret(), b"hello");
}
#[tokio::test]
async fn preserves_identity_from_inner() {
let inner = WithIdentity::new(BytesSource(b"aGVsbG8="), "kid-1");
let secret = MappedSecret::new(inner, Base64Encoding);
let out = secret.get_secret_value().await.unwrap();
assert_eq!(out.value.expose_secret(), b"hello");
assert_eq!(out.identity.as_deref(), Some("kid-1"));
}
#[tokio::test]
async fn surfaces_map_error_as_config_with_default_context() {
let secret = MappedSecret::new(BytesSource(b"not base64 !!"), Base64Encoding);
let err = secret.get_secret_value().await.unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
assert!(
err.to_string().contains("mapping secret value"),
"expected default context in: {err}"
);
}
#[tokio::test]
async fn context_prefixes_errors() {
let secret = MappedSecret::new(BytesSource(b"not base64 !!"), Base64Encoding)
.with_context("decoding the widget key");
let err = secret.get_secret_value().await.unwrap_err();
assert!(
err.to_string().contains("decoding the widget key"),
"expected custom context in: {err}"
);
}
#[tokio::test]
async fn chains_string_to_bytes_then_decode() {
let secret = StringSource("aGVsbG8=")
.mapped(StringToBytes)
.mapped(Base64Encoding);
let out = secret.get_secret_value().await.unwrap();
assert_eq!(out.value.expose_secret(), b"hello");
}
#[tokio::test]
async fn map_transforms_value_and_keeps_identity() {
let secret = WithIdentity::new(StringSource("hello"), "kid-1")
.map(|s: SecretString| SecretBytes::new(s.expose_secret().as_bytes().to_vec()));
let out = secret.get_secret_value().await.unwrap();
assert_eq!(out.value.expose_secret(), b"hello");
assert_eq!(out.identity.as_deref(), Some("kid-1"));
}
#[tokio::test]
async fn try_map_propagates_function_error() {
let secret = StringSource("hello")
.try_map(|_s| Err::<SecretBytes, _>(Error::from(ErrorKind::Config)));
let err = secret.get_secret_value().await.unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
}
#[tokio::test]
async fn try_map_passes_through_on_success() {
let secret = StringSource("42")
.try_map(|s: SecretString| Ok(SecretString::new(s.expose_secret().repeat(2))));
let out = secret.get_secret_value().await.unwrap();
assert_eq!(out.value.expose_secret(), "4242");
}
#[tokio::test]
async fn try_map_error_carries_context() {
let secret = StringSource("hello")
.try_map(|_s| Err::<SecretBytes, _>(Error::from(ErrorKind::Config)))
.with_context("parsing the vault payload");
let err = secret.get_secret_value().await.unwrap_err();
assert!(
err.to_string().contains("parsing the vault payload"),
"expected custom context in: {err}"
);
}
}