Skip to content

Commit

Permalink
Improve exposed dynamic redactions (mitsuhiko#81)
Browse files Browse the repository at this point in the history
This improves the dynamic redaction system by exposing a better API.

- remove pointless assertions
- added support for dynamic redactions in macro use
- expose some internals for doc purposes
  • Loading branch information
mitsuhiko authored Oct 23, 2019
1 parent 1169280 commit a49fae9
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 100 deletions.
32 changes: 28 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,26 @@
//! });
//! ```
//!
//! Through settings dynamic redactions can also be defined which are callback based
//! which can also be used for assertions. For more information see
//! [settings](struct.Settings.html).
//! It's also possible to execute a callback that can produce a new value
//! instead of hardcoding a replacement value by using the
//! [`dynamic_redaction`](fn.dynamic_redaction.html) function:
//!
//! ```rust,ignore
//! # #[derive(Serialize)]
//! # pub struct User {
//! # id: Uuid,
//! # username: String,
//! # }
//! assert_yaml_snapshot!(&User {
//! id: Uuid::new_v4(),
//! username: "john_doe".to_string(),
//! }, {
//! ".id" => dynamic_redaction(|value, _| {
//! // assert that the value looks like a uuid here
//! "[uuid]"
//! }),
//! });
//! ```
//!
//! # Inline Snapshots
//!
Expand Down Expand Up @@ -315,7 +332,10 @@ pub mod internals {
pub use crate::runtime::AutoName;
pub use crate::snapshot::{MetaData, SnapshotContents};
#[cfg(feature = "redactions")]
pub use crate::{redaction::ContentPath, settings::Redactions};
pub use crate::{
redaction::{ContentPath, Redaction},
settings::Redactions,
};
}

// exported for cargo-insta only
Expand All @@ -324,6 +344,10 @@ pub use crate::{
runtime::print_snapshot_diff, snapshot::PendingInlineSnapshot, snapshot::SnapshotContents,
};

// useful for redactions
#[cfg(feature = "redactions")]
pub use crate::redaction::dynamic_redaction;

// these are here to make the macros work
#[doc(hidden)]
pub mod _macro_support {
Expand Down
100 changes: 79 additions & 21 deletions src/redaction.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;

use pest::Parser;
use pest_derive::Parser;
use std::borrow::Cow;
use std::fmt;

use crate::content::Content;

Expand All @@ -21,6 +19,9 @@ impl SelectorParseError {
}

/// Represents a path for a callback function.
///
/// This can be converted into a string with `to_string` to see a stringified
/// path that the selector matched.
#[derive(Clone, Debug)]
pub struct ContentPath<'a>(&'a [PathItem]);

Expand All @@ -44,39 +45,96 @@ impl<'a> fmt::Display for ContentPath<'a> {
}
}

/// Asserts a value at a certain path.
type AssertionFunc = dyn Fn(&Content, ContentPath<'_>) + Sync + Send;

/// Replaces a value with another one.
type ReplacementFunc = dyn Fn(Content, ContentPath<'_>) -> Content + Sync + Send;
/// Types of redactions
#[derive(Clone)]
/// Represents a redaction.
pub enum Redaction {
/// Static redaction with new content.
Static(Content),
/// non-redaction but running an assertion function
Assertion(Arc<Box<AssertionFunc>>),
/// Redaction with new content.
Replacement(Arc<Box<ReplacementFunc>>),
Dynamic(Box<dyn Fn(Content, ContentPath<'_>) -> Content + Sync + Send>),
}

impl<T: Into<Content>> From<T> for Redaction {
fn from(value: T) -> Redaction {
Redaction::Static(value.into())
macro_rules! impl_from {
($ty:ty) => {
impl From<$ty> for Redaction {
fn from(value: $ty) -> Redaction {
Redaction::Static(Content::from(value))
}
}
};
}

impl_from!(());
impl_from!(bool);
impl_from!(u8);
impl_from!(u16);
impl_from!(u32);
impl_from!(u64);
impl_from!(i8);
impl_from!(i16);
impl_from!(i32);
impl_from!(i64);
impl_from!(f32);
impl_from!(f64);
impl_from!(char);
impl_from!(String);
impl_from!(Vec<u8>);

impl<'a> From<&'a str> for Redaction {
fn from(value: &'a str) -> Redaction {
Redaction::Static(Content::from(value))
}
}

impl<'a> From<&'a [u8]> for Redaction {
fn from(value: &'a [u8]) -> Redaction {
Redaction::Static(Content::from(value))
}
}

/// Creates a dynamic redaction.
///
/// This can be used to redact a value with a different value but instead of
/// statically declaring it a dynamic value can be computed. This can also
/// be used to perform assertions before replacing the value.
///
/// The closure is passed two arguments: the value as [`Content`](internals/enum.Content.html)
/// and the path that was selected (as [`ContentPath`](internals/struct.ContentPath.html)).
///
/// Example:
///
/// ```rust
/// # use insta::{Settings, dynamic_redaction};
/// # let mut settings = Settings::new();
/// settings.add_redaction(".id", dynamic_redaction(|value, path| {
/// assert_eq!(path.to_string(), ".id");
/// assert_eq!(
/// value
/// .as_str()
/// .unwrap()
/// .chars()
/// .filter(|&c| c == '-')
/// .count(),
/// 4
/// );
/// "[uuid]"
/// }));
/// ```
pub fn dynamic_redaction<I, F>(func: F) -> Redaction
where
I: Into<Content>,
F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static,
{
Redaction::Dynamic(Box::new(move |c, p| func(c, p).into()))
}

impl Redaction {
/// Performs the redaction of the value at the given path.
fn redact(&self, value: Content, path: &[PathItem]) -> Content {
match *self {
Redaction::Static(ref new_val) => new_val.clone(),
Redaction::Assertion(ref callback) => {
callback(&value, ContentPath(path));
value
}
Redaction::Replacement(ref callback) => callback(value, ContentPath(path)),
Redaction::Dynamic(ref callback) => callback(value, ContentPath(path)),
}
}
}
Expand Down
85 changes: 14 additions & 71 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::sync::Arc;
#[cfg(feature = "redactions")]
use crate::{
content::Content,
redaction::{ContentPath, Redaction, Selector},
redaction::{dynamic_redaction, ContentPath, Redaction, Selector},
};

lazy_static! {
Expand All @@ -22,20 +22,15 @@ thread_local!(static CURRENT_SETTINGS: RefCell<Settings> = RefCell::new(Settings
/// Represents stored redactions.
#[cfg(feature = "redactions")]
#[derive(Clone, Default)]
pub struct Redactions(Vec<(Selector<'static>, Redaction)>);
pub struct Redactions(Vec<(Selector<'static>, Arc<Redaction>)>);

#[cfg(feature = "redactions")]
impl<'a> From<Vec<(&'a str, Content)>> for Redactions {
fn from(value: Vec<(&'a str, Content)>) -> Redactions {
impl<'a> From<Vec<(&'a str, Redaction)>> for Redactions {
fn from(value: Vec<(&'a str, Redaction)>) -> Redactions {
Redactions(
value
.into_iter()
.map(|x| {
(
Selector::parse(x.0).unwrap().make_static(),
Redaction::Static(x.1),
)
})
.map(|x| (Selector::parse(x.0).unwrap().make_static(), Arc::new(x.1)))
.collect(),
)
}
Expand Down Expand Up @@ -121,10 +116,10 @@ impl Settings {
/// Note that this only applies to snapshots that undergo serialization
/// (eg: does not work for `assert_debug_snapshot!`.)
#[cfg(feature = "redactions")]
pub fn add_redaction<I: Into<Content>>(&mut self, selector: &str, replacement: I) {
pub fn add_redaction<R: Into<Redaction>>(&mut self, selector: &str, replacement: R) {
self._private_inner_mut().redactions.0.push((
Selector::parse(selector).unwrap().make_static(),
Redaction::Static(replacement.into()),
Arc::new(replacement.into()),
));
}

Expand All @@ -134,70 +129,14 @@ impl Settings {
/// asserts the value at a certain place. This function is internally
/// supposed to call things like `assert_eq!`.
///
/// Example:
///
/// ```rust
/// # use insta::Settings;
/// # let mut settings = Settings::new();
/// settings.add_dynamic_redaction(".id", |value, path| {
/// assert_eq!(path.to_string(), ".id");
/// assert_eq!(
/// value
/// .as_str()
/// .unwrap()
/// .chars()
/// .filter(|&c| c == '-')
/// .count(),
/// 4
/// );
/// "[uuid]"
/// });
/// ```
/// This is a shortcut to `add_redaction(dynamic_redaction(...))`;
#[cfg(feature = "redactions")]
pub fn add_dynamic_redaction<I, F>(&mut self, selector: &str, func: F)
where
I: Into<Content>,
F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static,
{
self._private_inner_mut().redactions.0.push((
Selector::parse(selector).unwrap().make_static(),
Redaction::Replacement(Arc::new(Box::new(move |c, p| func(c, p).into()))),
));
}

/// Registers an assertion callback.
///
/// This works similar to a redaction but instead of changing the value it
/// asserts the value at a certain place. This function is internally
/// supposed to call things like `assert_eq!`.
///
/// Example:
///
/// ```rust
/// # use insta::Settings;
/// # let mut settings = Settings::new();
/// settings.add_assertion(".id", |value, path| {
/// assert_eq!(path.to_string(), ".id");
/// assert_eq!(
/// value
/// .as_str()
/// .unwrap()
/// .chars()
/// .filter(|&c| c == '-')
/// .count(),
/// 4
/// );
/// });
/// ```
#[cfg(feature = "redactions")]
pub fn add_assertion<F>(&mut self, selector: &str, func: F)
where
F: Fn(&Content, ContentPath<'_>) + Send + Sync + 'static,
{
self._private_inner_mut().redactions.0.push((
Selector::parse(selector).unwrap().make_static(),
Redaction::Assertion(Arc::new(Box::new(func))),
));
self.add_redaction(selector, dynamic_redaction(func));
}

/// Replaces the currently set redactions.
Expand All @@ -217,7 +156,11 @@ impl Settings {
/// Iterate over the redactions.
#[cfg(feature = "redactions")]
pub(crate) fn iter_redactions(&self) -> impl Iterator<Item = (&Selector, &Redaction)> {
self.inner.redactions.0.iter().map(|&(ref a, ref b)| (a, b))
self.inner
.redactions
.0
.iter()
.map(|&(ref a, ref b)| (a, &**b))
}

/// Sets the snapshot path.
Expand Down
28 changes: 24 additions & 4 deletions tests/test_redaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ fn test_with_random_value() {
});
}

#[test]
fn test_with_random_value_inline_callback() {
assert_yaml_snapshot!("user", &User {
id: Uuid::new_v4(),
username: "john_doe".to_string(),
email: Email("[email protected]".to_string()),
extra: "".to_string(),
}, {
".id" => insta::dynamic_redaction(|value, path| {
assert_eq!(path.to_string(), ".id");
assert_eq!(
value
.as_str()
.unwrap()
.chars()
.filter(|&c| c == '-')
.count(),
4
);
"[uuid]"
}),
});
}

#[test]
fn test_with_random_value_and_trailing_comma() {
assert_yaml_snapshot!("user", &User {
Expand Down Expand Up @@ -119,10 +143,6 @@ fn test_with_callbacks() {
);
"[uuid]"
});
settings.add_assertion(".extra", |value, path| {
assert_eq!(path.to_string(), ".extra");
assert_eq!(value.as_str(), Some("extra here"));
});
settings.bind(|| {
assert_json_snapshot!(
"user_json_settings_callback",
Expand Down

0 comments on commit a49fae9

Please sign in to comment.