Repository: mitsuhiko/insta
Pull request #81 (2019-10-23 13:28:01)

Improve exposed dynamic redactions
This improves the dynamic redaction system by exposing a better API.


Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 5b74682..66c40e3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
 //!
@@ -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
@@ -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 {
diff --git a/src/redaction.rs b/src/redaction.rs
index 6b33bce..76a59b4 100644
--- a/src/redaction.rs
+++ b/src/redaction.rs
@@ -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;
 
@@ -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]);
 
@@ -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)),
         }
     }
 }
diff --git a/src/settings.rs b/src/settings.rs
index 0e11a67..e7828b6 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -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! {
@@ -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(),
         )
     }
@@ -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()),
         ));
     }
 
@@ -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.
@@ -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.


Test patch:
diff --git a/tests/test_redaction.rs b/tests/test_redaction.rs
index 3846962..c285424 100644
--- a/tests/test_redaction.rs
+++ b/tests/test_redaction.rs
@@ -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("john@example.com".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 {
@@ -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",