blob: 1b058869b8834e7c1dd14b75dabc9a5ad2593b6c [file] [log] [blame]
use pest::Parser;
use pest_derive::Parser;
use std::borrow::Cow;
use std::fmt;
use crate::content::Content;
#[derive(Debug)]
pub struct SelectorParseError(pest::error::Error<Rule>);
impl SelectorParseError {
/// Return the column of where the error ocurred.
pub fn column(&self) -> usize {
match self.0.line_col {
pest::error::LineColLocation::Pos((_, col)) => col,
pest::error::LineColLocation::Span((_, col), _) => col,
}
}
}
/// 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]);
impl<'a> fmt::Display for ContentPath<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for item in self.0.iter() {
write!(f, ".")?;
match *item {
PathItem::Content(ref ctx) => {
if let Some(s) = ctx.as_str() {
write!(f, "{}", s)?;
} else {
write!(f, "<content>")?;
}
}
PathItem::Field(name) => write!(f, "{}", name)?,
PathItem::Index(idx, _) => write!(f, "{}", idx)?,
}
}
Ok(())
}
}
/// Replaces a value with another one.
/// Represents a redaction.
pub enum Redaction {
/// Static redaction with new content.
Static(Content),
/// Redaction with new content.
Dynamic(Box<dyn Fn(Content, ContentPath<'_>) -> Content + Sync + Send>),
}
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::Dynamic(ref callback) => callback(value, ContentPath(path)),
}
}
}
#[derive(Parser)]
#[grammar = "select_grammar.pest"]
pub struct SelectParser;
#[derive(Debug)]
pub enum PathItem {
Content(Content),
Field(&'static str),
Index(u64, u64),
}
impl PathItem {
fn as_str(&self) -> Option<&str> {
match *self {
PathItem::Content(ref content) => content.as_str(),
PathItem::Field(s) => Some(s),
PathItem::Index(..) => None,
}
}
fn as_u64(&self) -> Option<u64> {
match *self {
PathItem::Content(ref content) => content.as_u64(),
PathItem::Field(_) => None,
PathItem::Index(idx, _) => Some(idx),
}
}
fn range_check(&self, start: Option<i64>, end: Option<i64>) -> bool {
fn expand_range(sel: i64, len: i64) -> i64 {
if sel < 0 {
(len + sel).max(0)
} else {
sel
}
}
let (idx, len) = match *self {
PathItem::Index(idx, len) => (idx as i64, len as i64),
_ => return false,
};
match (start, end) {
(None, None) => true,
(None, Some(end)) => idx < expand_range(end, len),
(Some(start), None) => idx >= expand_range(start, len),
(Some(start), Some(end)) => {
idx >= expand_range(start, len) && idx < expand_range(end, len)
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Segment<'a> {
DeepWildcard,
Wildcard,
Key(Cow<'a, str>),
Index(u64),
Range(Option<i64>, Option<i64>),
}
#[derive(Debug, Clone)]
pub struct Selector<'a> {
selectors: Vec<Vec<Segment<'a>>>,
}
impl<'a> Selector<'a> {
pub fn parse(selector: &'a str) -> Result<Selector<'a>, SelectorParseError> {
let pair = SelectParser::parse(Rule::selectors, selector)
.map_err(SelectorParseError)?
.next()
.unwrap();
let mut rv = vec![];
for selector_pair in pair.into_inner() {
match selector_pair.as_rule() {
Rule::EOI => break,
other => assert_eq!(other, Rule::selector),
}
let mut segments = vec![];
let mut have_deep_wildcard = false;
for segment_pair in selector_pair.into_inner() {
segments.push(match segment_pair.as_rule() {
Rule::identity => continue,
Rule::wildcard => Segment::Wildcard,
Rule::deep_wildcard => {
if have_deep_wildcard {
return Err(SelectorParseError(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "deep wildcard used twice".into(),
},
segment_pair.as_span(),
)));
}
have_deep_wildcard = true;
Segment::DeepWildcard
}
Rule::key => Segment::Key(Cow::Borrowed(&segment_pair.as_str()[1..])),
Rule::subscript => {
let subscript_rule = segment_pair.into_inner().next().unwrap();
match subscript_rule.as_rule() {
Rule::int => Segment::Index(subscript_rule.as_str().parse().unwrap()),
Rule::string => {
let sq = subscript_rule.as_str();
let s = &sq[1..sq.len() - 1];
let mut was_backslash = false;
Segment::Key(if s.bytes().any(|x| x == b'\\') {
Cow::Owned(
s.chars()
.filter_map(|c| {
let rv = match c {
'\\' if !was_backslash => {
was_backslash = true;
return None;
}
other => other,
};
was_backslash = false;
Some(rv)
})
.collect(),
)
} else {
Cow::Borrowed(s)
})
}
_ => unreachable!(),
}
}
Rule::full_range => Segment::Range(None, None),
Rule::range => {
let mut int_rule = segment_pair
.into_inner()
.map(|x| x.as_str().parse().unwrap());
Segment::Range(int_rule.next(), int_rule.next())
}
Rule::range_to => {
let int_rule = segment_pair.into_inner().next().unwrap();
Segment::Range(None, int_rule.as_str().parse().ok())
}
Rule::range_from => {
let int_rule = segment_pair.into_inner().next().unwrap();
Segment::Range(int_rule.as_str().parse().ok(), None)
}
_ => unreachable!(),
});
}
rv.push(segments);
}
Ok(Selector { selectors: rv })
}
pub fn make_static(self) -> Selector<'static> {
Selector {
selectors: self
.selectors
.into_iter()
.map(|parts| {
parts
.into_iter()
.map(|x| match x {
Segment::Key(x) => Segment::Key(Cow::Owned(x.into_owned())),
Segment::Index(x) => Segment::Index(x),
Segment::Wildcard => Segment::Wildcard,
Segment::DeepWildcard => Segment::DeepWildcard,
Segment::Range(a, b) => Segment::Range(a, b),
})
.collect()
})
.collect(),
}
}
fn segment_is_match(&self, segment: &Segment, element: &PathItem) -> bool {
match *segment {
Segment::Wildcard => true,
Segment::DeepWildcard => true,
Segment::Key(ref k) => element.as_str() == Some(&k),
Segment::Index(i) => element.as_u64() == Some(i),
Segment::Range(start, end) => element.range_check(start, end),
}
}
fn selector_is_match(&self, selector: &[Segment], path: &[PathItem]) -> bool {
if let Some(idx) = selector.iter().position(|x| *x == Segment::DeepWildcard) {
let forward_sel = &selector[..idx];
let backward_sel = &selector[idx + 1..];
if path.len() <= idx {
return false;
}
for (segment, element) in forward_sel.iter().zip(path.iter()) {
if !self.segment_is_match(segment, element) {
return false;
}
}
for (segment, element) in backward_sel.iter().rev().zip(path.iter().rev()) {
if !self.segment_is_match(segment, element) {
return false;
}
}
true
} else {
if selector.len() != path.len() {
return false;
}
for (segment, element) in selector.iter().zip(path.iter()) {
if !self.segment_is_match(segment, element) {
return false;
}
}
true
}
}
pub fn is_match(&self, path: &[PathItem]) -> bool {
for selector in &self.selectors {
if self.selector_is_match(&selector, path) {
return true;
}
}
false
}
pub fn redact(&self, value: Content, redaction: &Redaction) -> Content {
self.redact_impl(value, redaction, &mut vec![])
}
fn redact_seq(
&self,
seq: Vec<Content>,
redaction: &Redaction,
path: &mut Vec<PathItem>,
) -> Vec<Content> {
let len = seq.len();
seq.into_iter()
.enumerate()
.map(|(idx, value)| {
path.push(PathItem::Index(idx as u64, len as u64));
let new_value = self.redact_impl(value, redaction, path);
path.pop();
new_value
})
.collect()
}
fn redact_struct(
&self,
seq: Vec<(&'static str, Content)>,
redaction: &Redaction,
path: &mut Vec<PathItem>,
) -> Vec<(&'static str, Content)> {
seq.into_iter()
.map(|(key, value)| {
path.push(PathItem::Field(key));
let new_value = self.redact_impl(value, redaction, path);
path.pop();
(key, new_value)
})
.collect()
}
fn redact_impl(
&self,
value: Content,
redaction: &Redaction,
path: &mut Vec<PathItem>,
) -> Content {
if self.is_match(&path) {
redaction.redact(value, path)
} else {
match value {
Content::Map(map) => Content::Map(
map.into_iter()
.map(|(key, value)| {
path.push(PathItem::Content(key.clone()));
let new_value = self.redact_impl(value, redaction, path);
path.pop();
(key, new_value)
})
.collect(),
),
Content::Seq(seq) => Content::Seq(self.redact_seq(seq, redaction, path)),
Content::Tuple(seq) => Content::Tuple(self.redact_seq(seq, redaction, path)),
Content::TupleStruct(name, seq) => {
Content::TupleStruct(name, self.redact_seq(seq, redaction, path))
}
Content::TupleVariant(name, variant_index, variant, seq) => Content::TupleVariant(
name,
variant_index,
variant,
self.redact_seq(seq, redaction, path),
),
Content::Struct(name, seq) => {
Content::Struct(name, self.redact_struct(seq, redaction, path))
}
Content::StructVariant(name, variant_index, variant, seq) => {
Content::StructVariant(
name,
variant_index,
variant,
self.redact_struct(seq, redaction, path),
)
}
Content::NewtypeStruct(name, inner) => Content::NewtypeStruct(
name,
Box::new(self.redact_impl(*inner, redaction, path)),
),
Content::Some(contents) => {
Content::Some(Box::new(self.redact_impl(*contents, redaction, path)))
}
other => other,
}
}
}
}
#[test]
fn test_range_checks() {
assert_eq!(PathItem::Index(0, 10).range_check(None, Some(-1)), true);
assert_eq!(PathItem::Index(9, 10).range_check(None, Some(-1)), false);
assert_eq!(PathItem::Index(0, 10).range_check(Some(1), Some(-1)), false);
assert_eq!(PathItem::Index(1, 10).range_check(Some(1), Some(-1)), true);
assert_eq!(PathItem::Index(9, 10).range_check(Some(1), Some(-1)), false);
assert_eq!(PathItem::Index(0, 10).range_check(Some(1), None), false);
assert_eq!(PathItem::Index(1, 10).range_check(Some(1), None), true);
assert_eq!(PathItem::Index(9, 10).range_check(Some(1), None), true);
}