0

I have this big rust struct (DictWordMetadata), with more structs contained inside it. I have to check the value of a specific field in this struct or the structs contained inside it agains the value of a filter. This filter comes in JSON form, but I currently deserialize it to the same type (DictWordMetadata).

To give an example to make the problem clearer, the filter can come as the following JSON:

"metadata_condition": {
  "stress": "Oxitona"
},

The struct I was referring to has the following format:

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Hash)]
pub struct DictWordMetadata {
    pub noun: Option<NounData>,
    pub pronoun: Option<PronounData>,
    pub verb: Option<VerbData>,
    pub adjective: Option<AdjectiveData>,
    pub adverb: Option<AdverbData>,
    pub conjunction: Option<ConjunctionData>,
    pub swear: Option<bool>,
    /// The dialects this word belongs to.
    /// If no dialects are defined, it can be assumed that the word is
    /// valid in all dialects of English.
    #[serde(default = "default_default")]
    pub dialects: DialectFlags,
    /// Orthographic information: letter case, spaces, hyphens, etc.
    #[serde(default = "OrthFlags::empty")]
    pub orth_info: OrthFlags,
    /// Whether the word is a [determiner](https://en.wikipedia.org/wiki/English_determiners).
    pub determiner: Option<DeterminerData>,
    /// Whether the word is a [preposition](https://www.merriam-webster.com/dictionary/preposition).
    #[serde(default = "default_false")]
    pub preposition: bool,
    /// Whether the word is considered especially common.
    #[serde(default = "default_false")]
    pub common: bool,
    #[serde(default = "default_none")]
    pub derived_from: Option<WordId>,
    /// Generated by a chunker
    pub np_member: Option<bool>,
    /// Generated by a POS tagger
    pub pos_tag: Option<UPOS>,

    // The two under here are only used in the Portuguese grammar
    /// In which of the last three syllables of the word is the stress
    pub stress: Option<Stress>,
    /// Words in portuguese are split between masculine and feminine
    pub gender: Option<Gender>,
}

So I'd have to check if the stress on my initial object (not the filter) has the same value of the value in the filter, and if so I return true. But I have to do this for every possible value, including recursively. I imagine the solution has something to do with macros, but just with declarative macros I couldn't get anywhere, and proc macros might me overkill (all the trouble of setting up a separate crate just for a minor function). Any ideas on how to solve this?

I also made this attempt, but as you can probably notice it doesn't have the recursivity I wanted nor does it work well to begin with, because it compares the whole object after extracting it from the Option, instead of comparing only the stuff that was set

macro_rules! check_metadata_condition {
        ( $obj:expr, $cond:expr, $($field:ident),+ $(,)? ) => {{
            $(
                if let (Some(equal), Some(cond_val)) = (&$obj.$field, &$cond.$field) {
                    if obj_val != cond_val {
                        return false;
                    }
                }
            )+
            true
        }};
    }

To be clear, I'm not asking for anyone to implement this, although some examples would be great. Just asking for some guidance of how to approach this problem in the first place, maybe changing the way I deserialize the filter to begin with or something like this. Thank you.

1
  • It is possible to use serde itself for this, however this won't be short, and unless you have really many fields, or many structs, just filtering explicitly will be shorter (or maybe there is a crate that does that). Commented Nov 12 at 17:43

2 Answers 2

1

I'd create a trait for that

pub trait FilterCondition {
    fn filter_condition(&self, other: &Self) -> bool;
}

// impl for base elements
impl FilterCondition for u8 {
    fn filter_condition(&self, other: &Self) -> bool {
        (self & other) > 0
    }
}

// impl for all optional type
impl<T: FilterCondition> FilterCondition for Option<T> {
    fn filter_condition(&self, other: &Self) -> bool {
        match (self, other) {
            (Some(a), Some(b)) => a.filter_condition(b),
            _ => false,
        }
    }
}

and use macro for derive, you can use proc-macro, for even better ergonomic but it's more annoying.


// Macro for deriving FilterCondition
#[macro_export]
macro_rules! derive_filter_condition {
    ($st:ty, $($fields:ident),+) => {
        impl FilterCondition for $st {
            fn filter_condition(&self, other: &Self) -> bool {
                derive_filter_condition_inner!(self, other, $($fields),+);

                false
            }
        }
    };
}

// Inner Macro for deriving, no need be public
macro_rules! derive_filter_condition_inner {
    ($self:ident, $other:ident, $x:ident) => {
        if $self.$x.filter_condition(&$other.$x) { return true; }
    };
    ($self:ident, $other:ident, $x:ident, $($y:ident),+) => {
        derive_filter_condition_inner!($self, $other, $x);
        derive_filter_condition_inner!($self, $other, $($y),+)
    }
}

then you can use it on your types like this

#[derive(Debug, Clone, Default, PartialEq)]
pub struct MetaA {
    pub inner_a: Option<u8>,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct Metadata {
    pub a: Option<MetaA>,
    pub b: Option<u8>,
    pub c: u8,
}

derive_filter_condition!(MetaA, inner_a);
derive_filter_condition!(Metadata, a, b, c);
//                                 ^  ^  ^ 
//                 just list all the field here (that implemented FilterCondition)
//                 this is where proc-macro might improve ergonomic, but more work

Examples

fn main() {
    let meta = Metadata {
        a: Some(MetaA { inner_a: Some(0b1) }),
        b: Some(0b11),
        c: 0b111,
    };

    let filter_1 = Metadata {
        ..Default::default()
    };
    assert_eq!(false, filter_1.filter_condition(&meta));

    let filter_2 = Metadata {
        c: 0b100,
        ..Default::default()
    };
    assert_eq!(true, filter_2.filter_condition(&meta));

    let filter_3 = Metadata {
        a: Some(MetaA {
            inner_a: Some(0b11),
        }),
        ..Default::default()
    };
    assert_eq!(true, filter_3.filter_condition(&meta));
}

cargo expand

this is what the macro expanded to

impl FilterCondition for MetaA {
    fn filter_condition(&self, other: &Self) -> bool {
        if self.inner_a.filter_condition(&other.inner_a) {
            return true;
        }
        false
    }
}
impl FilterCondition for Metadata {
    fn filter_condition(&self, other: &Self) -> bool {
        if self.a.filter_condition(&other.a) {
            return true;
        }
        if self.b.filter_condition(&other.b) {
            return true;
        }
        if self.c.filter_condition(&other.c) {
            return true;
        }
        false
    }
}
Sign up to request clarification or add additional context in comments.

Comments

-1

To achieve what you want, I think the best way would be to have a dedicated filter struct and a corresponding trait. I explain:

Let's say you have this simple struct:


struct SomeData {
    a_field: String,
    other_field: usize,
}

One example of a filter struct could be:


struct SomeDataFilter {
    a_field: Option<String>,
    other_field: Option<usize>,
}

And the trait will be:


trait Filter<F> {
    fn matches(&self, filter: F) -> bool;
}

Given that, you can easily filter SomeData with SomeDataFilter. You can write a procedural macro to generate the ...Filter struct and implement Filter<TFilter> for T. And, later, if you need it, you'll be able to add more complex filters like one-of, any-expected, regex, ...

Concerning the derivation of Filter, I would recommend to do the following: call self.field.filter(filter.field) for each field. It allows the recursion for every type that does implement Filter. Then, you can manually (more or less) implement the trait for the native Rust types, with many variants like Filter<Option<usize>> for usize, Filter<Vec<usize>> for usize, ...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.