diff --git a/src/lib.rs b/src/lib.rs index 5b746823..66c40e3f 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 6b33bce5..76a59b47 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>), /// Redaction with new content. - Replacement(Arc>), + Dynamic(Box) -> Content + Sync + Send>), } -impl> From 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); + +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(func: F) -> Redaction +where + I: Into, + 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 0e11a675..e7828b69 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 = 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)>); #[cfg(feature = "redactions")] -impl<'a> From> for Redactions { - fn from(value: Vec<(&'a str, Content)>) -> Redactions { +impl<'a> From> 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>(&mut self, selector: &str, replacement: I) { + pub fn add_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(&mut self, selector: &str, func: F) where I: Into, 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(&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 { - 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. diff --git a/tests/test_redaction.rs b/tests/test_redaction.rs index 38469625..c285424a 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",