use crate::engine::error::{DataflowError, Result};
use crate::engine::utils::get_nested_value;
use datalogic_rs::bumpalo::Bump;
use datalogic_rs::operator::EvalContext;
use datalogic_rs::{CustomOperator, DataValue, Error as LogicError};
use datavalue::OwnedDataValue;
use std::fmt;
use std::sync::Arc;
pub const SECRET_OPERATOR: &str = "secret";
pub struct Secrets {
root: OwnedDataValue,
}
pub(crate) static EMPTY: Secrets = Secrets::empty();
impl Secrets {
pub(crate) const fn empty() -> Self {
Self {
root: OwnedDataValue::Object(Vec::new()),
}
}
pub(crate) fn new(root: OwnedDataValue) -> Result<Self> {
if !root.is_object() {
return Err(DataflowError::Validation(
"secrets must be a JSON object of name -> value".to_string(),
));
}
Ok(Self { root })
}
pub fn get(&self, path: &str) -> Option<&OwnedDataValue> {
if path.is_empty() {
return None;
}
get_nested_value(&self.root, path)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.root
.as_object()
.into_iter()
.flatten()
.map(|(k, _)| k.as_str())
}
}
impl fmt::Debug for Secrets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Secrets");
for name in self.names() {
s.field(name, &"******");
}
s.finish()
}
}
pub(crate) struct SecretOperator(pub(crate) Arc<Secrets>);
impl CustomOperator for SecretOperator {
fn evaluate<'a>(
&self,
args: &[&'a DataValue<'a>],
_ctx: &mut EvalContext<'_, 'a>,
arena: &'a Bump,
) -> datalogic_rs::Result<&'a DataValue<'a>> {
let key = match args {
[DataValue::String(s)] if !s.is_empty() => *s,
[DataValue::String(_)] => {
return Err(LogicError::invalid_arguments(
"secret: the key must not be empty",
));
}
[_] => {
return Err(LogicError::invalid_arguments(
"secret: the key must be a string",
));
}
_ => {
return Err(LogicError::invalid_arguments(format!(
"secret: takes exactly one argument, got {}",
args.len()
)));
}
};
match self.0.get(key) {
Some(value) => Ok(arena.alloc(value.to_arena(arena))),
None => Err(LogicError::variable_not_found(format!(
"secret '{key}' is not declared"
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn store() -> Secrets {
Secrets::new(OwnedDataValue::from(&json!({
"api_token": "tok-value-9f8e",
"partner": { "hmac": "hmac-value-1a2b" }
})))
.unwrap()
}
#[test]
fn debug_prints_names_and_masks_every_value() {
let rendered = format!("{:?}", store());
assert_eq!(
rendered,
r#"Secrets { api_token: "******", partner: "******" }"#
);
}
#[test]
fn the_empty_path_is_not_the_whole_store() {
assert!(store().get("").is_none());
assert_eq!(
store().get("partner.hmac"),
Some(&OwnedDataValue::from(&json!("hmac-value-1a2b")))
);
assert!(store().get("partner.nope").is_none());
}
#[test]
fn only_objects_are_accepted() {
assert!(Secrets::new(OwnedDataValue::from(&json!(["a"]))).is_err());
assert!(Secrets::new(OwnedDataValue::from(&json!("s"))).is_err());
assert_eq!(Secrets::empty().names().count(), 0);
}
}