Skip to content

Commit

Permalink
Added support for a sorting redaction (#212)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored Jan 25, 2022
1 parent 0a81677 commit 9fc10eb
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to insta and cargo-insta are documented here.

## 1.12.0

- Add support for sorting redactions (`sorted_redaction` and `Settings::sort_selector`). (#212)

## 1.11.0

- Trim down some unnecessary dependencies and switch to `once_cell`. (#208)
Expand Down
17 changes: 14 additions & 3 deletions src/content.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// this module is based on the content module in serde::private::ser
use serde::ser::{self, Serialize, Serializer};
use std::cmp::Ordering;
use std::marker::PhantomData;

/// Represents variable typed content.
Expand All @@ -24,7 +25,7 @@ use std::marker::PhantomData;
///
/// If you do need to pattern match you should use the
/// `resolve_inner` method to resolve such internal wrappers.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Content {
Bool(bool),

Expand Down Expand Up @@ -98,8 +99,8 @@ pub enum Key<'a> {
impl<'a> Eq for Key<'a> {}

impl<'a> Ord for Key<'a> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Less)
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Less)
}
}

Expand Down Expand Up @@ -170,6 +171,16 @@ impl Content {
}
}

/// Mutable version of [`resolve_inner`](Self::resolve_inner).
pub fn resolve_inner_mut(&mut self) -> &mut Content {
match *self {
Content::Some(ref mut v)
| Content::NewtypeStruct(_, ref mut v)
| Content::NewtypeVariant(_, _, _, ref mut v) => v.resolve_inner_mut(),
ref mut other => other,
}
}

/// Returns the value as string
pub fn as_str(&self) -> Option<&str> {
match self.resolve_inner() {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ pub mod _cargo_insta_support {

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

// these are here to make the macros work
#[doc(hidden)]
Expand Down
33 changes: 33 additions & 0 deletions src/redaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ where
Redaction::Dynamic(Box::new(move |c, p| func(c, p).into()))
}

/// Creates a dynamic redaction that sorts the value at the selector.
///
/// This is useful to force something like a set or map to be ordered to make
/// it deterministic. This is necessary as insta's serialization support is
/// based on serde which does not have native set support. As a result vectors
/// (which need to retain order) and sets (which should be given a stable order)
/// look the same.
///
/// ```rust
/// # use insta::{Settings, sorted_redaction};
/// # let mut settings = Settings::new();
/// settings.add_redaction(".flags", sorted_redaction());
/// ```
pub fn sorted_redaction() -> Redaction {
fn sort(mut value: Content, _path: ContentPath) -> Content {
match value.resolve_inner_mut() {
Content::Seq(ref mut val) => {
val.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
Content::Map(ref mut val) => {
val.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
Content::Struct(_, ref mut fields)
| Content::StructVariant(_, _, _, ref mut fields) => {
fields.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
_ => {}
}
value
}
dynamic_redaction(sort)
}

impl Redaction {
/// Performs the redaction of the value at the given path.
fn redact(&self, value: Content, path: &[PathItem]) -> Content {
Expand Down
12 changes: 10 additions & 2 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use once_cell::sync::Lazy;
#[cfg(feature = "redactions")]
use crate::{
content::Content,
redaction::{dynamic_redaction, ContentPath, Redaction, Selector},
redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector},
};

static DEFAULT_SETTINGS: Lazy<Arc<ActualSettings>> = Lazy::new(|| {
Expand Down Expand Up @@ -235,7 +235,7 @@ impl Settings {
/// asserts the value at a certain place. This function is internally
/// supposed to call things like `assert_eq!`.
///
/// This is a shortcut to `add_redaction(dynamic_redaction(...))`;
/// This is a shortcut to `add_redaction(selector, dynamic_redaction(...))`;
#[cfg(feature = "redactions")]
pub fn add_dynamic_redaction<I, F>(&mut self, selector: &str, func: F)
where
Expand All @@ -245,6 +245,14 @@ impl Settings {
self.add_redaction(selector, dynamic_redaction(func));
}

/// A special redaction that sorts a sequence or map.
///
/// This is a shortcut to `add_redaction(selector, sorted_redaction())`.
#[cfg(feature = "redactions")]
pub fn sort_selector(&mut self, selector: &str) {
self.add_redaction(selector, sorted_redaction());
}

/// Replaces the currently set redactions.
///
/// The default set is empty.
Expand Down
16 changes: 16 additions & 0 deletions tests/snapshots/test_redaction__user_json_flags.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: tests/test_redaction.rs
assertion_line: 359
expression: "&User{id: 122,\n username: \"jason_doe\".to_string(),\n flags:\n vec![\"zzz\".into(), \"foo\".into(), \"aha\".into(),\n \"is_admin\".into()].into_iter().collect(),}"

---
{
"id": "[id]",
"username": "jason_doe",
"flags": [
"aha",
"foo",
"is_admin",
"zzz"
]
}
16 changes: 16 additions & 0 deletions tests/snapshots/test_redaction__user_json_flags_alt.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: tests/test_redaction.rs
assertion_line: 384
expression: "&User{id: 122,\n username: \"jason_doe\".to_string(),\n flags:\n MySet(vec![\"zzz\".into(), \"foo\".into(), \"aha\".into(),\n \"is_admin\".into()].into_iter().collect()),}"

---
{
"flags": [
"aha",
"foo",
"is_admin",
"zzz"
],
"id": 122,
"username": "jason_doe"
}
59 changes: 57 additions & 2 deletions tests/test_redaction.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#![cfg(feature = "redactions")]

use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};

use insta::_macro_support::Selector;
use insta::{
assert_debug_snapshot, assert_json_snapshot, assert_yaml_snapshot, with_settings, Settings,
assert_debug_snapshot, assert_json_snapshot, assert_yaml_snapshot, sorted_redaction,
with_settings, Settings,
};
use serde::Serialize;

Expand Down Expand Up @@ -341,3 +342,57 @@ fn test_map_key_redaction() {
".btm.$key" => "[key]",
});
}

#[test]
fn test_ordering() {
#[derive(Debug, Serialize)]
pub struct User {
id: u64,
username: String,
flags: HashSet<String>,
}

let mut settings = Settings::new();
settings.add_redaction(".id", "[id]");
settings.sort_selector(".flags");
settings.bind(|| {
assert_json_snapshot!(
"user_json_flags",
&User {
id: 122,
username: "jason_doe".to_string(),
flags: vec!["zzz".into(), "foo".into(), "aha".into(), "is_admin".into()]
.into_iter()
.collect(),
}
);
});
}

#[test]
fn test_ordering_newtype_set() {
#[derive(Debug, Serialize)]
pub struct MySet(HashSet<String>);

#[derive(Debug, Serialize)]
pub struct User {
id: u64,
username: String,
flags: MySet,
}

assert_json_snapshot!(
"user_json_flags_alt",
&User {
id: 122,
username: "jason_doe".to_string(),
flags: MySet(vec!["zzz".into(), "foo".into(), "aha".into(), "is_admin".into()]
.into_iter()
.collect()),
},
{
"." => sorted_redaction(),
".flags" => sorted_redaction()
}
);
}

0 comments on commit 9fc10eb

Please sign in to comment.