How to tell if a struct contains a key


#1

Wondering if there is a good way to determine if a Struct contains a specific key. For instance, I have the following struct:

pub struct AuthError {
    error: String,
    error_description: String
}

and I want to be able to dynamically determine if that struct has a key named ‘error’ using something like the following:

pub fn do_something<T>(obj: &mut T) {
    let has_error = has_key!(T, "error");
    if (has_error) {
        // Do something with T.error
    }
    else {...}
}

do_something<AuthError>(&mut myError);

#2

It sounds like you’re looking for traits:

trait MaybeError {
    fn get_error(&self) -> &str;
}

impl MaybeError for AuthError {
    fn get_error(&self) -> Option<&str> {
        Some(&self.error)
    }
}

pub fn do_something<T: MaybeError>(obj: &mut T) {
    if let Some(err) = obj.get_error() {
        ...
    } else {
        ...
    }
}

#3

Only problem is I am trying to do this in such a way that I can iterate over all of the keys in any struct. Maybe a better implementation would be a function, or macro, that returns a Vec of the struct’s keys.

pub struct AuthError {
    error: String,
    error_description: String
}

get_keys!(AuthError) // => Vec { "error", "error_description" }
// Or
get_keys<AuthError>();

#4

You mean “struct field names”. It’s easy to do in D language. But I don’t know how do to it in Rust.


#5

Yeah I guess fields names is better than keys. I was trying to think of the correct terminology. Closest answer I was able to find is this http://stackoverflow.com/questions/29986057/get-fields-of-a-struct-type-in-a-macro and this http://stackoverflow.com/questions/37140768/how-to-get-struct-field-names-in-rust. But I would prefer to be able to do it in such a way that I don’t have to pass in a whole struct, just the Ident


#6

Why are you trying to iterate over all of the field names? And is it just the names, or do you plan to do something to the values?

This sounds like the sort of thing macros and compiler plugins are written for (like for custom #[derive(Trait)]s), and is certainly not something one can easily do in rust without a significant time investment.

For the most common use case of serialization to a string, there is std::fmt::Debug (e.g. println!("{:?}", x);) and serde (for e.g. JSON).


Alternatively, maybe a Map type can suit your needs? (though all values will need to be the same type, or at least share a Trait in common)


#7

Ditto what @ExpHP wrote, but speaking of Serde here is an implementation that uses Serde to pull out the fields names of any struct that implements Serde’s Deserialize trait. For any type T: Deserialize it gives you a &'static [&'static str] of the field names. Undefined what happens if T is not a struct.

#![feature(plugin, custom_derive)]
#![plugin(serde_macros)]

#[macro_use]
extern crate serde;

#[derive(Deserialize)]
pub struct AuthError {
    error: String,
    error_description: String
}

pub fn struct_fields<T: serde::Deserialize>() -> &'static [&'static str] {
    struct StructFieldsDeserializer(Option<&'static [&'static str]>);

    impl serde::Deserializer for StructFieldsDeserializer {
        type Error = serde::de::value::Error;

        fn deserialize<V>(&mut self, _visitor: V) -> Result<V::Value, Self::Error>
            where V: serde::de::Visitor
        {
            Err(serde::de::Error::custom("I'm just here for the fields"))
        }

        fn deserialize_struct<V>(&mut self,
                                 _name: &'static str,
                                 fields: &'static [&'static str],
                                 visitor: V) -> Result<V::Value, Self::Error>
            where V: serde::de::Visitor
        {
            self.0 = Some(fields);
            self.deserialize(visitor)
        }

        forward_to_deserialize! {
            bool usize u8 u16 u32 u64 isize i8 i16 i32 i64 f32 f64 char str string
            unit option seq seq_fixed_size bytes map unit_struct newtype_struct
            tuple_struct struct_field tuple enum ignored_any
        }
    }

    let mut de = StructFieldsDeserializer(None);
    T::deserialize(&mut de).ok();
    de.0.unwrap()
}

fn main() {
    // prints ["error", "error_description"]
    println!("{:?}", struct_fields::<AuthError>());
}

#8

There’s also something I want to say about this.

struct UnixError {
    error: u32,
    error_description: String,
}
struct StringError {
    error: String,
    error_description: String,
}

pub fn do_something<T>(obj: &mut T) {
    let x = obj.error;
    x.insert(1, 'b'); // possible on strings, but not integers
}

// somewhere else...
do_something(z);

Food for thought: In do_something, is the x.insert line an error?

In Python, a dynamic language, either line in the function could cause the program to die at runtime with an AttributeError if the member doesn’t exist.

In C++, it depends on what you call it with. Calling the function with a UnixError will cause an error at compile time. Calling it with a StringError will compile just fine. This is because C++ does its name resolution and type checking “during template instantiation” (which is to say, when it already knows the type of obj and all its members).


Compared to the above languages, Rust is a bit of a paradigm shift. In Rust, it is always possible to determine whether a piece of code can be compiled successfully without looking at how it is used. Any input types consistent with the type signature—actual or theoretical—must all be fair game.

For this analysis, the actual types are irrelevant, so let’s throw them away!

pub fn do_something<T>(obj: &mut T) {
    let x = obj.error;
    x.insert(1, 'b'); // possible on strings, but not integers
}

Looking at this piece of code, we see a function that promises to work with all possible types T, but in actuality expects the type to have an “error” member. Because the function fails to uphold its own promises, it fails to compile, even if it is never used.

This is where the importance of traits come in:

trait AsError {
    type Error;
    fn as_error(&self) -> &Self::Error;
}

pub fn do_something<T>(obj: &T)
 where T: AsError
{
    let x = obj.as_error();
}

By adding a trait bound to do_something, we have restricted its promises. And (again without even looking at any types) we can see this time that this function will always compile succesfully given types that meet its signature. This is because it now only promises to work for types that implement AsError, which provides an as_error method.

In this manner, traits are an unavoidable part of any sort of polymorphic code in Rust—at least, outside of *shudders* macros.


(never mind std::mem::size_of() or anything else which is made of black magic (but size_of is the example people keep going to))